diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.js
index c4fb469323d9d9373f74fd726afa5b12f1bc056a..aa23d041867c2c68816d37b343ca4dab18793e43 100644
--- a/packages/desktop-client/e2e/page-models/account-page.js
+++ b/packages/desktop-client/e2e/page-models/account-page.js
@@ -106,7 +106,7 @@ export class AccountPage {
     await this.menuButton.click();
     await this.page.getByRole('button', { name: 'Close Account' }).click();
     return new CloseAccountModal(
-      this.page.locator('css=[aria-modal]'),
+      this.page.getByTestId('close-account-modal'),
       this.page,
     );
   }
diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png
index 171ae0751a2e5a7e649672bbc41d46f13d599d69..7ee3aa5afa31bb300e22542905fba7cbc6769e66 100644
Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png differ
diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx
index 01f1505f40f1fbec7ccde907965ff20422477090..07711a2d2a3ce25be823134e3cddf7c8ac21753c 100644
--- a/packages/desktop-client/src/components/Modals.tsx
+++ b/packages/desktop-client/src/components/Modals.tsx
@@ -1,17 +1,17 @@
 // @ts-strict-ignore
 import React, { useEffect } from 'react';
-import { useSelector } from 'react-redux';
+import { useDispatch } from 'react-redux';
 import { useLocation } from 'react-router-dom';
 
-import { type State } from 'loot-core/src/client/state-types';
+import { closeModal } from 'loot-core/client/actions';
 import { type PopModalAction } from 'loot-core/src/client/state-types/modals';
 import { send } from 'loot-core/src/platform/client/fetch';
 import * as monthUtils from 'loot-core/src/shared/months';
 
-import { useActions } from '../hooks/useActions';
+import { useModalState } from '../hooks/useModalState';
 import { useSyncServerStatus } from '../hooks/useSyncServerStatus';
 
-import { ModalTitle } from './common/Modal';
+import { ModalTitle, ModalHeader } from './common/Modal2';
 import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal';
 import { AccountMenuModal } from './modals/AccountMenuModal';
 import { BudgetListModal } from './modals/BudgetListModal';
@@ -71,67 +71,43 @@ export type CommonModalProps = {
 };
 
 export function Modals() {
-  const modalStack = useSelector((state: State) => state.modals.modalStack);
-  const isHidden = useSelector((state: State) => state.modals.isHidden);
-  const actions = useActions();
   const location = useLocation();
+  const dispatch = useDispatch();
+  const { modalStack } = useModalState();
 
   useEffect(() => {
     if (modalStack.length > 0) {
-      actions.closeModal();
+      dispatch(closeModal());
     }
   }, [location]);
 
   const syncServerStatus = useSyncServerStatus();
 
   const modals = modalStack
-    .map(({ name, options }, idx) => {
-      const modalProps: CommonModalProps = {
-        onClose: actions.popModal,
-        onBack: actions.popModal,
-        showBack: idx > 0,
-        isCurrent: idx === modalStack.length - 1,
-        isHidden,
-        stackIndex: idx,
-      };
-
+    .map(({ name, options }) => {
       switch (name) {
         case 'keyboard-shortcuts':
-          return <KeyboardShortcutModal modalProps={modalProps} />;
+          return <KeyboardShortcutModal />;
 
         case 'import-transactions':
-          return (
-            <ImportTransactions
-              key={name}
-              modalProps={modalProps}
-              options={options}
-            />
-          );
+          return <ImportTransactions key={name} options={options} />;
 
         case 'add-account':
           return (
             <CreateAccountModal
               key={name}
-              modalProps={modalProps}
               syncServerStatus={syncServerStatus}
               upgradingAccountId={options?.upgradingAccountId}
             />
           );
 
         case 'add-local-account':
-          return (
-            <CreateLocalAccountModal
-              key={name}
-              modalProps={modalProps}
-              actions={actions}
-            />
-          );
+          return <CreateLocalAccountModal key={name} />;
 
         case 'close-account':
           return (
             <CloseAccountModal
               key={name}
-              modalProps={modalProps}
               account={options.account}
               balance={options.balance}
               canDelete={options.canDelete}
@@ -142,10 +118,8 @@ export function Modals() {
           return (
             <SelectLinkedAccounts
               key={name}
-              modalProps={modalProps}
               externalAccounts={options.accounts}
               requisitionId={options.requisitionId}
-              actions={actions}
               syncSource={options.syncSource}
             />
           );
@@ -154,7 +128,6 @@ export function Modals() {
           return (
             <ConfirmCategoryDelete
               key={name}
-              modalProps={modalProps}
               category={options.category}
               group={options.group}
               onDelete={options.onDelete}
@@ -165,7 +138,6 @@ export function Modals() {
           return (
             <ConfirmUnlinkAccount
               key={name}
-              modalProps={modalProps}
               accountName={options.accountName}
               onUnlink={options.onUnlink}
             />
@@ -175,7 +147,6 @@ export function Modals() {
           return (
             <ConfirmTransactionEdit
               key={name}
-              modalProps={modalProps}
               onCancel={options.onCancel}
               onConfirm={options.onConfirm}
               confirmReason={options.confirmReason}
@@ -186,7 +157,6 @@ export function Modals() {
           return (
             <ConfirmTransactionDelete
               key={name}
-              modalProps={modalProps}
               onConfirm={options.onConfirm}
             />
           );
@@ -197,26 +167,17 @@ export function Modals() {
               key={name}
               watchUpdates
               budgetId={options.budgetId}
-              modalProps={modalProps}
-              actions={actions}
               backupDisabled={false}
             />
           );
 
         case 'manage-rules':
-          return (
-            <ManageRulesModal
-              key={name}
-              modalProps={modalProps}
-              payeeId={options?.payeeId}
-            />
-          );
+          return <ManageRulesModal key={name} payeeId={options?.payeeId} />;
 
         case 'edit-rule':
           return (
             <EditRule
               key={name}
-              modalProps={modalProps}
               defaultRule={options.rule}
               onSave={options.onSave}
             />
@@ -226,7 +187,6 @@ export function Modals() {
           return (
             <MergeUnusedPayees
               key={name}
-              modalProps={modalProps}
               payeeIds={options.payeeIds}
               targetPayeeId={options.targetPayeeId}
             />
@@ -234,27 +194,18 @@ export function Modals() {
 
         case 'gocardless-init':
           return (
-            <GoCardlessInitialise
-              key={name}
-              modalProps={modalProps}
-              onSuccess={options.onSuccess}
-            />
+            <GoCardlessInitialise key={name} onSuccess={options.onSuccess} />
           );
 
         case 'simplefin-init':
           return (
-            <SimpleFinInitialise
-              key={name}
-              modalProps={modalProps}
-              onSuccess={options.onSuccess}
-            />
+            <SimpleFinInitialise key={name} onSuccess={options.onSuccess} />
           );
 
         case 'gocardless-external-msg':
           return (
             <GoCardlessExternalMsg
               key={name}
-              modalProps={modalProps}
               onMoveExternal={options.onMoveExternal}
               onClose={() => {
                 options.onClose?.();
@@ -265,28 +216,15 @@ export function Modals() {
           );
 
         case 'create-encryption-key':
-          return (
-            <CreateEncryptionKeyModal
-              key={name}
-              modalProps={modalProps}
-              options={options}
-            />
-          );
+          return <CreateEncryptionKeyModal key={name} options={options} />;
 
         case 'fix-encryption-key':
-          return (
-            <FixEncryptionKeyModal
-              key={name}
-              modalProps={modalProps}
-              options={options}
-            />
-          );
+          return <FixEncryptionKeyModal key={name} options={options} />;
 
         case 'edit-field':
           return (
             <EditField
               key={name}
-              modalProps={modalProps}
               name={options.name}
               onSubmit={options.onSubmit}
               onClose={options.onClose}
@@ -297,7 +235,6 @@ export function Modals() {
           return (
             <CategoryAutocompleteModal
               key={name}
-              modalProps={modalProps}
               autocompleteProps={{
                 value: null,
                 onSelect: options.onSelect,
@@ -313,7 +250,6 @@ export function Modals() {
           return (
             <AccountAutocompleteModal
               key={name}
-              modalProps={modalProps}
               autocompleteProps={{
                 value: null,
                 onSelect: options.onSelect,
@@ -327,7 +263,6 @@ export function Modals() {
           return (
             <PayeeAutocompleteModal
               key={name}
-              modalProps={modalProps}
               autocompleteProps={{
                 value: null,
                 onSelect: options.onSelect,
@@ -340,8 +275,13 @@ export function Modals() {
           return (
             <SingleInputModal
               key={name}
-              modalProps={modalProps}
-              title={<ModalTitle title="New Category" shrinkOnOverflow />}
+              name={name}
+              Header={props => (
+                <ModalHeader
+                  {...props}
+                  title={<ModalTitle title="New Category" shrinkOnOverflow />}
+                />
+              )}
               inputPlaceholder="Category name"
               buttonText="Add"
               onValidate={options.onValidate}
@@ -353,8 +293,15 @@ export function Modals() {
           return (
             <SingleInputModal
               key={name}
-              modalProps={modalProps}
-              title={<ModalTitle title="New Category Group" shrinkOnOverflow />}
+              name={name}
+              Header={props => (
+                <ModalHeader
+                  {...props}
+                  title={
+                    <ModalTitle title="New Category Group" shrinkOnOverflow />
+                  }
+                />
+              )}
               inputPlaceholder="Category group name"
               buttonText="Add"
               onValidate={options.onValidate}
@@ -370,7 +317,6 @@ export function Modals() {
             >
               <RolloverBudgetSummaryModal
                 key={name}
-                modalProps={modalProps}
                 month={options.month}
                 onBudgetAction={options.onBudgetAction}
               />
@@ -378,21 +324,13 @@ export function Modals() {
           );
 
         case 'report-budget-summary':
-          return (
-            <ReportBudgetSummaryModal
-              key={name}
-              modalProps={modalProps}
-              month={options.month}
-            />
-          );
+          return <ReportBudgetSummaryModal key={name} month={options.month} />;
 
         case 'schedule-edit':
           return (
             <ScheduleDetails
               key={name}
-              modalProps={modalProps}
               id={options?.id || null}
-              actions={actions}
               transaction={options?.transaction || null}
             />
           );
@@ -401,36 +339,21 @@ export function Modals() {
           return (
             <ScheduleLink
               key={name}
-              modalProps={modalProps}
-              actions={actions}
               transactionIds={options?.transactionIds}
               getTransaction={options?.getTransaction}
             />
           );
 
         case 'schedules-discover':
-          return (
-            <DiscoverSchedules
-              key={name}
-              modalProps={modalProps}
-              actions={actions}
-            />
-          );
+          return <DiscoverSchedules key={name} />;
 
         case 'schedule-posts-offline-notification':
-          return (
-            <PostsOfflineNotification
-              key={name}
-              modalProps={modalProps}
-              actions={actions}
-            />
-          );
+          return <PostsOfflineNotification key={name} />;
 
         case 'account-menu':
           return (
             <AccountMenuModal
               key={name}
-              modalProps={modalProps}
               accountId={options.accountId}
               onSave={options.onSave}
               onEditNotes={options.onEditNotes}
@@ -444,11 +367,11 @@ export function Modals() {
           return (
             <CategoryMenuModal
               key={name}
-              modalProps={modalProps}
               categoryId={options.categoryId}
               onSave={options.onSave}
               onEditNotes={options.onEditNotes}
               onDelete={options.onDelete}
+              onToggleVisibility={options.onToggleVisibility}
               onClose={options.onClose}
             />
           );
@@ -460,7 +383,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <RolloverBudgetMenuModal
-                modalProps={modalProps}
                 categoryId={options.categoryId}
                 onUpdateBudget={options.onUpdateBudget}
                 onCopyLastMonthAverage={options.onCopyLastMonthAverage}
@@ -477,7 +399,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <ReportBudgetMenuModal
-                modalProps={modalProps}
                 categoryId={options.categoryId}
                 onUpdateBudget={options.onUpdateBudget}
                 onCopyLastMonthAverage={options.onCopyLastMonthAverage}
@@ -491,13 +412,13 @@ export function Modals() {
           return (
             <CategoryGroupMenuModal
               key={name}
-              modalProps={modalProps}
               groupId={options.groupId}
               onSave={options.onSave}
               onAddCategory={options.onAddCategory}
               onEditNotes={options.onEditNotes}
               onSaveNotes={options.onSaveNotes}
               onDelete={options.onDelete}
+              onToggleVisibility={options.onToggleVisibility}
               onClose={options.onClose}
             />
           );
@@ -506,7 +427,6 @@ export function Modals() {
           return (
             <NotesModal
               key={name}
-              modalProps={modalProps}
               id={options.id}
               name={options.name}
               onSave={options.onSave}
@@ -520,7 +440,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <RolloverBalanceMenuModal
-                modalProps={modalProps}
                 categoryId={options.categoryId}
                 onCarryover={options.onCarryover}
                 onTransfer={options.onTransfer}
@@ -536,7 +455,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <RolloverToBudgetMenuModal
-                modalProps={modalProps}
                 onTransfer={options.onTransfer}
                 onCover={options.onCover}
                 onHoldBuffer={options.onHoldBuffer}
@@ -552,7 +470,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <HoldBufferModal
-                modalProps={modalProps}
                 month={options.month}
                 onSubmit={options.onSubmit}
               />
@@ -566,7 +483,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <ReportBalanceMenuModal
-                modalProps={modalProps}
                 categoryId={options.categoryId}
                 onCarryover={options.onCarryover}
               />
@@ -577,7 +493,6 @@ export function Modals() {
           return (
             <TransferModal
               key={name}
-              modalProps={modalProps}
               title={options.title}
               month={options.month}
               amount={options.amount}
@@ -590,7 +505,6 @@ export function Modals() {
           return (
             <CoverModal
               key={name}
-              modalProps={modalProps}
               title={options.title}
               month={options.month}
               showToBeBudgeted={options.showToBeBudgeted}
@@ -602,7 +516,6 @@ export function Modals() {
           return (
             <ScheduledTransactionMenuModal
               key={name}
-              modalProps={modalProps}
               transactionId={options.transactionId}
               onPost={options.onPost}
               onSkip={options.onSkip}
@@ -613,7 +526,6 @@ export function Modals() {
           return (
             <BudgetPageMenuModal
               key={name}
-              modalProps={modalProps}
               onAddCategoryGroup={options.onAddCategoryGroup}
               onToggleHiddenCategories={options.onToggleHiddenCategories}
               onSwitchBudgetFile={options.onSwitchBudgetFile}
@@ -627,7 +539,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <RolloverBudgetMonthMenuModal
-                modalProps={modalProps}
                 month={options.month}
                 onBudgetAction={options.onBudgetAction}
                 onEditNotes={options.onEditNotes}
@@ -642,7 +553,6 @@ export function Modals() {
               value={monthUtils.sheetForMonth(options.month)}
             >
               <ReportBudgetMonthMenuModal
-                modalProps={modalProps}
                 month={options.month}
                 onBudgetAction={options.onBudgetAction}
                 onEditNotes={options.onEditNotes}
@@ -651,7 +561,7 @@ export function Modals() {
           );
 
         case 'budget-list':
-          return <BudgetListModal key={name} modalProps={modalProps} />;
+          return <BudgetListModal key={name} />;
 
         default:
           console.error('Unknown modal:', name);
diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx
index 802bac03d159a75534c21d85e2d68e273d5ea114..e3a65d694b2adef0384767acd945c5d114c287c6 100644
--- a/packages/desktop-client/src/components/common/Modal.tsx
+++ b/packages/desktop-client/src/components/common/Modal.tsx
@@ -322,7 +322,8 @@ type ModalButtonsProps = {
   children: ReactNode;
 };
 
-export const ModalButtons = ({
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const ModalButtons = ({
   style,
   leftContent,
   focusButton = false,
@@ -367,7 +368,7 @@ type ModalTitleProps = {
   shrinkOnOverflow?: boolean;
 };
 
-export function ModalTitle({
+function ModalTitle({
   title,
   isEditable,
   getStyle,
@@ -466,7 +467,7 @@ type ModalCloseButtonProps = {
   style?: CSSProperties;
 };
 
-export function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) {
+function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) {
   return (
     <Button
       type="bare"
diff --git a/packages/desktop-client/src/components/common/Modal2.tsx b/packages/desktop-client/src/components/common/Modal2.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..84fab17c4c2f6c7919c1a854fa53c8e89e4a386b
--- /dev/null
+++ b/packages/desktop-client/src/components/common/Modal2.tsx
@@ -0,0 +1,460 @@
+import React, {
+  useEffect,
+  useRef,
+  useLayoutEffect,
+  useState,
+  type ReactNode,
+  type ComponentPropsWithoutRef,
+  type ComponentPropsWithRef,
+} from 'react';
+import {
+  ModalOverlay as ReactAriaModalOverlay,
+  Modal as ReactAriaModal,
+  Dialog,
+} from 'react-aria-components';
+import { useHotkeysContext } from 'react-hotkeys-hook';
+
+import { AutoTextSize } from 'auto-text-size';
+
+import { useModalState } from '../../hooks/useModalState';
+import { AnimatedLoading } from '../../icons/AnimatedLoading';
+import { SvgLogo } from '../../icons/logo';
+import { SvgDelete } from '../../icons/v0';
+import { type CSSProperties, styles, theme } from '../../style';
+import { tokens } from '../../tokens';
+
+import { Button } from './Button';
+import { Input } from './Input';
+import { Text } from './Text';
+import { TextOneLine } from './TextOneLine';
+import { View } from './View';
+
+type ModalProps = ComponentPropsWithRef<typeof ReactAriaModal> & {
+  name: string;
+  isLoading?: boolean;
+  noAnimation?: boolean;
+  style?: CSSProperties;
+  onClose?: () => void;
+  containerProps?: {
+    style?: CSSProperties;
+  };
+};
+
+export const Modal = ({
+  name,
+  isLoading = false,
+  noAnimation = false,
+  style,
+  children,
+  onClose,
+  containerProps,
+  ...props
+}: ModalProps) => {
+  const { enableScope, disableScope } = useHotkeysContext();
+
+  // This deactivates any key handlers in the "app" scope
+  useEffect(() => {
+    enableScope(name);
+    return () => disableScope(name);
+  }, [enableScope, disableScope, name]);
+
+  const { isHidden, isActive, onClose: closeModal } = useModalState();
+
+  const handleOnClose = () => {
+    closeModal();
+    onClose?.();
+  };
+
+  return (
+    <ReactAriaModalOverlay
+      data-testid={`${name}-modal`}
+      isDismissable
+      defaultOpen={true}
+      onOpenChange={isOpen => !isOpen && handleOnClose?.()}
+      style={{
+        position: 'fixed',
+        inset: 0,
+        zIndex: 3000,
+        overflowY: 'auto',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+        fontSize: 14,
+        ...style,
+      }}
+      {...props}
+    >
+      <ReactAriaModal>
+        {modalProps => (
+          <Dialog aria-label="Modal dialog">
+            <ModalContentContainer
+              noAnimation={noAnimation}
+              isActive={isActive(name)}
+              {...containerProps}
+              style={{
+                flex: 1,
+                padding: 10,
+                willChange: 'opacity, transform',
+                maxWidth: '90vw',
+                minWidth: '90vw',
+                maxHeight: '90vh',
+                minHeight: 0,
+                borderRadius: 6,
+                //border: '1px solid ' + theme.modalBorder,
+                color: theme.pageText,
+                backgroundColor: theme.modalBackground,
+                opacity: isHidden ? 0 : 1,
+                [`@media (min-width: ${tokens.breakpoint_small})`]: {
+                  minWidth: tokens.breakpoint_small,
+                },
+                ...styles.shadowLarge,
+                ...styles.lightScrollbar,
+                ...containerProps?.style,
+              }}
+            >
+              <View style={{ paddingTop: 0, flex: 1 }}>
+                {typeof children === 'function'
+                  ? children(modalProps)
+                  : children}
+              </View>
+              {isLoading && (
+                <View
+                  style={{
+                    position: 'absolute',
+                    top: 0,
+                    left: 0,
+                    right: 0,
+                    bottom: 0,
+                    backgroundColor: theme.pageBackground,
+                    alignItems: 'center',
+                    justifyContent: 'center',
+                    zIndex: 1000,
+                  }}
+                >
+                  <AnimatedLoading
+                    style={{ width: 20, height: 20 }}
+                    color={theme.pageText}
+                  />
+                </View>
+              )}
+            </ModalContentContainer>
+          </Dialog>
+        )}
+      </ReactAriaModal>
+    </ReactAriaModalOverlay>
+  );
+};
+
+type ModalContentContainerProps = {
+  style?: CSSProperties;
+  noAnimation?: boolean;
+  isActive?: boolean;
+  children: ReactNode;
+};
+
+const ModalContentContainer = ({
+  style,
+  noAnimation,
+  isActive,
+  children,
+}: ModalContentContainerProps) => {
+  const contentRef = useRef<HTMLDivElement>(null);
+  const mounted = useRef(false);
+  const rotateFactor = useRef(Math.random() * 10 - 5);
+
+  useLayoutEffect(() => {
+    if (!contentRef.current) {
+      return;
+    }
+
+    function setProps() {
+      if (!contentRef.current) {
+        return;
+      }
+
+      if (isActive) {
+        contentRef.current.style.transform = 'translateY(0px) scale(1)';
+        contentRef.current.style.pointerEvents = 'auto';
+      } else {
+        contentRef.current.style.transform = `translateY(-40px) scale(.95) rotate(${rotateFactor.current}deg)`;
+        contentRef.current.style.pointerEvents = 'none';
+      }
+    }
+
+    if (!mounted.current) {
+      if (noAnimation) {
+        contentRef.current.style.opacity = '1';
+        contentRef.current.style.transform = 'translateY(0px) scale(1)';
+
+        setTimeout(() => {
+          if (contentRef.current) {
+            contentRef.current.style.transition =
+              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
+          }
+        }, 0);
+      } else {
+        contentRef.current.style.opacity = '0';
+        contentRef.current.style.transform = 'translateY(10px) scale(1)';
+
+        setTimeout(() => {
+          if (contentRef.current) {
+            mounted.current = true;
+            contentRef.current.style.transition =
+              'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)';
+            contentRef.current.style.opacity = '1';
+            setProps();
+          }
+        }, 0);
+      }
+    } else {
+      setProps();
+    }
+  }, [noAnimation, isActive]);
+
+  return (
+    <View
+      innerRef={contentRef}
+      style={{
+        ...style,
+        ...(noAnimation && !isActive && { display: 'none' }),
+      }}
+    >
+      {children}
+    </View>
+  );
+};
+
+type ModalButtonsProps = {
+  style?: CSSProperties;
+  leftContent?: ReactNode;
+  focusButton?: boolean;
+  children: ReactNode;
+};
+
+export const ModalButtons = ({
+  style,
+  leftContent,
+  focusButton = false,
+  children,
+}: ModalButtonsProps) => {
+  const containerRef = useRef<HTMLDivElement>(null);
+
+  useEffect(() => {
+    if (focusButton && containerRef.current) {
+      const button = containerRef.current.querySelector<HTMLButtonElement>(
+        'button:not([data-hidden])',
+      );
+
+      if (button) {
+        button.focus();
+      }
+    }
+  }, [focusButton]);
+
+  return (
+    <View
+      innerRef={containerRef}
+      style={{
+        flexDirection: 'row',
+        marginTop: 30,
+        ...style,
+      }}
+    >
+      {leftContent}
+      <View style={{ flex: 1 }} />
+      {children}
+    </View>
+  );
+};
+
+type ModalHeaderProps = {
+  leftContent?: ReactNode;
+  showLogo?: boolean;
+  title?: ReactNode;
+  rightContent?: ReactNode;
+};
+
+export function ModalHeader({
+  leftContent,
+  showLogo,
+  title,
+  rightContent,
+}: ModalHeaderProps) {
+  return (
+    <View
+      aria-label="Modal header"
+      style={{
+        justifyContent: 'center',
+        alignItems: 'center',
+        position: 'relative',
+        height: 60,
+      }}
+    >
+      <View
+        style={{
+          position: 'absolute',
+          left: 0,
+        }}
+      >
+        {leftContent}
+      </View>
+
+      {(title || showLogo) && (
+        <View
+          style={{
+            textAlign: 'center',
+            // We need to force a width for the text-overflow
+            // ellipses to work because we are aligning center.
+            width: 'calc(100% - 60px)',
+          }}
+        >
+          {showLogo && (
+            <SvgLogo
+              width={30}
+              height={30}
+              style={{ justifyContent: 'center', alignSelf: 'center' }}
+            />
+          )}
+          {title &&
+            (typeof title === 'string' || typeof title === 'number' ? (
+              <ModalTitle title={`${title}`} />
+            ) : (
+              title
+            ))}
+        </View>
+      )}
+
+      {rightContent && (
+        <View
+          style={{
+            position: 'absolute',
+            right: 0,
+          }}
+        >
+          {rightContent}
+        </View>
+      )}
+    </View>
+  );
+}
+
+type ModalTitleProps = {
+  title: string;
+  isEditable?: boolean;
+  getStyle?: (isEditing: boolean) => CSSProperties;
+  onEdit?: (isEditing: boolean) => void;
+  onTitleUpdate?: (newName: string) => void;
+  shrinkOnOverflow?: boolean;
+};
+
+export function ModalTitle({
+  title,
+  isEditable,
+  getStyle,
+  onTitleUpdate,
+  shrinkOnOverflow = false,
+}: ModalTitleProps) {
+  const [isEditing, setIsEditing] = useState(false);
+
+  const onTitleClick = () => {
+    if (isEditable) {
+      setIsEditing(true);
+    }
+  };
+
+  const _onTitleUpdate = (newTitle: string) => {
+    if (newTitle !== title) {
+      onTitleUpdate?.(newTitle);
+    }
+    setIsEditing(false);
+  };
+
+  const inputRef = useRef<HTMLInputElement>(null);
+  useEffect(() => {
+    if (isEditing) {
+      if (inputRef.current) {
+        inputRef.current.scrollLeft = 0;
+      }
+    }
+  }, [isEditing]);
+
+  const style = getStyle?.(isEditing);
+
+  return isEditing ? (
+    <Input
+      inputRef={inputRef}
+      style={{
+        fontSize: 25,
+        fontWeight: 700,
+        textAlign: 'center',
+        ...style,
+      }}
+      focused={isEditing}
+      defaultValue={title}
+      onUpdate={_onTitleUpdate}
+      onKeyDown={e => {
+        if (e.key === 'Enter') {
+          e.preventDefault();
+          _onTitleUpdate?.(e.currentTarget.value);
+        }
+      }}
+    />
+  ) : (
+    <View
+      style={{
+        flexDirection: 'row',
+        justifyContent: 'center',
+        alignItems: 'center',
+      }}
+    >
+      {shrinkOnOverflow ? (
+        <AutoTextSize
+          as={Text}
+          minFontSizePx={15}
+          maxFontSizePx={25}
+          onClick={onTitleClick}
+          style={{
+            fontSize: 25,
+            fontWeight: 700,
+            textAlign: 'center',
+            ...(isEditable && styles.underlinedText),
+            ...style,
+          }}
+        >
+          {title}
+        </AutoTextSize>
+      ) : (
+        <TextOneLine
+          onClick={onTitleClick}
+          style={{
+            fontSize: 25,
+            fontWeight: 700,
+            textAlign: 'center',
+            ...(isEditable && styles.underlinedText),
+            ...style,
+          }}
+        >
+          {title}
+        </TextOneLine>
+      )}
+    </View>
+  );
+}
+
+type ModalCloseButtonProps = {
+  onClick: ComponentPropsWithoutRef<typeof Button>['onClick'];
+  style?: CSSProperties;
+};
+
+export function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) {
+  return (
+    <Button
+      type="bare"
+      onClick={onClick}
+      style={{ padding: '10px 10px' }}
+      aria-label="Close"
+    >
+      <SvgDelete width={10} style={style} />
+    </Button>
+  );
+}
diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx
index 7da6005d2e90aeb8590efcfc40fbfea00d529e35..4bac2bee7a7711c6ea53c2f1570d84a761cd4ee4 100644
--- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx
+++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx
@@ -70,7 +70,10 @@ function ToBudget({ toBudget, onClick, show3Cols }) {
       <Button
         type="bare"
         style={{ maxWidth: sidebarColumnWidth }}
-        onClick={onClick}
+        onPointerUp={e => {
+          e.stopPropagation();
+          onClick?.();
+        }}
       >
         <View>
           <Label
@@ -140,7 +143,10 @@ function Saved({ projected, onClick, show3Cols }) {
       <Button
         type="bare"
         style={{ maxWidth: sidebarColumnWidth }}
-        onClick={onClick}
+        onPointerUp={e => {
+          e.stopPropagation();
+          onClick?.();
+        }}
       >
         <View>
           <View>
@@ -267,7 +273,10 @@ function BudgetCell({
       type="financial"
       getStyle={makeAmountGrey}
       data-testid={name}
-      onClick={onOpenCategoryBudgetMenu}
+      onPointerUp={e => {
+        e.stopPropagation();
+        onOpenCategoryBudgetMenu();
+      }}
       {...props}
     />
   );
@@ -436,7 +445,10 @@ const ExpenseCategory = memo(function ExpenseCategory({
           style={{
             maxWidth: sidebarColumnWidth,
           }}
-          onClick={() => onEdit?.(category.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onEdit?.(category.id);
+          }}
         >
           <View
             style={{
@@ -525,7 +537,10 @@ const ExpenseCategory = memo(function ExpenseCategory({
             binding={spent}
             getStyle={makeAmountGrey}
             type="financial"
-            onClick={onShowActivity}
+            onPointerUp={e => {
+              e.stopPropagation();
+              onShowActivity();
+            }}
             formatter={value => (
               <Button
                 type="bare"
@@ -560,7 +575,13 @@ const ExpenseCategory = memo(function ExpenseCategory({
             width: columnWidth,
           }}
         >
-          <span role="button" onClick={() => onOpenBalanceMenu?.()}>
+          <span
+            role="button"
+            onPointerUp={e => {
+              e.stopPropagation();
+              onOpenBalanceMenu();
+            }}
+          >
             <BalanceWithCarryover
               carryover={carryover}
               balance={balance}
@@ -710,7 +731,10 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({
           hoveredStyle={{
             backgroundColor: 'transparent',
           }}
-          onClick={() => onToggleCollapse?.(group.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onToggleCollapse?.(group.id);
+          }}
         >
           <SvgExpandArrow
             width={8}
@@ -727,7 +751,10 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({
           style={{
             maxWidth: sidebarColumnWidth,
           }}
-          onClick={() => onEdit?.(group.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onEdit?.(group.id);
+          }}
         >
           <View
             style={{
@@ -862,7 +889,7 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({
       {/* {editMode && (
         <View>
           <Button
-            onClick={() => onAddCategory(group.id, group.is_income)}
+            onPointerUp={() => onAddCategory(group.id, group.is_income)}
             style={{ padding: 10 }}
           >
             <Add width={15} height={15} />
@@ -937,7 +964,10 @@ const IncomeGroupHeader = memo(function IncomeGroupHeader({
           hoveredStyle={{
             backgroundColor: 'transparent',
           }}
-          onClick={() => onToggleCollapse?.(group.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onToggleCollapse?.(group.id);
+          }}
         >
           <SvgExpandArrow
             width={8}
@@ -954,7 +984,10 @@ const IncomeGroupHeader = memo(function IncomeGroupHeader({
           style={{
             maxWidth: sidebarColumnWidth,
           }}
-          onClick={() => onEdit?.(group.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onEdit?.(group.id);
+          }}
         >
           <View
             style={{
@@ -1100,7 +1133,10 @@ const IncomeCategory = memo(function IncomeCategory({
           style={{
             maxWidth: sidebarColumnWidth,
           }}
-          onClick={() => onEdit?.(category.id)}
+          onPointerUp={e => {
+            e.stopPropagation();
+            onEdit?.(category.id);
+          }}
         >
           <View
             style={{
@@ -1225,20 +1261,20 @@ const IncomeCategory = memo(function IncomeCategory({
 //         <MathOperations emitter={emitter} />
 //         <View style={{ flex: 1 }} />
 //         <Button
-//           onClick={() => emitter.emit('moveUp')}
+//           onPointerUp={() => emitter.emit('moveUp')}
 //           style={{ marginRight: 5 }}
 //           data-testid="up"
 //         >
 //           <ArrowThinUp width={13} height={13} />
 //         </Button>
 //         <Button
-//           onClick={() => emitter.emit('moveDown')}
+//           onPointerUp={() => emitter.emit('moveDown')}
 //           style={{ marginRight: 5 }}
 //           data-testid="down"
 //         >
 //           <ArrowThinDown width={13} height={13} />
 //         </Button>
-//         <Button onClick={() => emitter.emit('done')} data-testid="done">
+//         <Button onPointerUp={() => emitter.emit('done')} data-testid="done">
 //           Done
 //         </Button>
 //       </View>
@@ -1627,7 +1663,10 @@ export function BudgetTable({
               }}
               hoveredStyle={noBackgroundColorStyle}
               activeStyle={noBackgroundColorStyle}
-              onClick={() => onOpenBudgetPageMenu?.()}
+              onPointerUp={e => {
+                e.stopPropagation();
+                onOpenBudgetPageMenu?.();
+              }}
             >
               <SvgLogo width="20" height="20" />
               <SvgCheveronRight
@@ -1751,7 +1790,10 @@ function BudgetTableHeader({
             <Button
               type="bare"
               disabled={show3Cols}
-              onClick={toggleSpentColumn}
+              onPointerUp={e => {
+                e.stopPropagation();
+                toggleSpentColumn();
+              }}
               style={buttonStyle}
             >
               <View style={{ alignItems: 'flex-end' }}>
@@ -1812,7 +1854,10 @@ function BudgetTableHeader({
             <Button
               type="bare"
               disabled={show3Cols}
-              onClick={toggleSpentColumn}
+              onPointerUp={e => {
+                e.stopPropagation();
+                toggleSpentColumn();
+              }}
               style={buttonStyle}
             >
               <View style={{ alignItems: 'flex-end' }}>
@@ -1926,7 +1971,12 @@ function MonthSelector({
     >
       <Button
         type="bare"
-        onClick={prevEnabled && onPrevMonth}
+        onPointerUp={e => {
+          e.stopPropagation();
+          if (prevEnabled) {
+            onPrevMonth();
+          }
+        }}
         style={{
           ...styles.noTapHighlight,
           ...arrowButtonStyle,
@@ -1949,13 +1999,21 @@ function MonthSelector({
           margin: '0 5px',
           ...styles.underlinedText,
         }}
-        onClick={() => onOpenMonthMenu?.(month)}
+        onPointerUp={e => {
+          e.stopPropagation();
+          onOpenMonthMenu?.(month);
+        }}
       >
         {monthUtils.format(month, 'MMMM ‘yy')}
       </Text>
       <Button
         type="bare"
-        onClick={nextEnabled && onNextMonth}
+        onPointerUp={e => {
+          e.stopPropagation();
+          if (nextEnabled) {
+            onNextMonth();
+          }
+        }}
         style={{
           ...styles.noTapHighlight,
           ...arrowButtonStyle,
diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx
index 9fcc4ab9ab34a26a9040798d5647a1e13848b939..91e84611503f6fd20a2ab24efa766c377ad03dec 100644
--- a/packages/desktop-client/src/components/mobile/budget/index.tsx
+++ b/packages/desktop-client/src/components/mobile/budget/index.tsx
@@ -169,6 +169,15 @@ function BudgetInner(props: BudgetInnerProps) {
     }
   };
 
+  const onToggleGroupVisibility = groupId => {
+    const group = categoryGroups.find(g => g.id === groupId);
+    onSaveGroup({
+      ...group,
+      hidden: !!!group.hidden,
+    });
+    dispatch(collapseModals('category-group-menu'));
+  };
+
   const onSaveCategory = category => {
     dispatch(updateCategory(category));
   };
@@ -196,6 +205,15 @@ function BudgetInner(props: BudgetInnerProps) {
     }
   };
 
+  const onToggleCategoryVisibility = categoryId => {
+    const category = categories.find(c => c.id === categoryId);
+    onSaveCategory({
+      ...category,
+      hidden: !!!category.hidden,
+    });
+    dispatch(collapseModals('category-menu'));
+  };
+
   const onReorderCategory = (id, { inGroup, aroundCategory }) => {
     let groupId, targetId;
 
@@ -323,6 +341,7 @@ function BudgetInner(props: BudgetInnerProps) {
         onAddCategory: onOpenNewCategoryModal,
         onEditNotes: onOpenCategoryGroupNotesModal,
         onDelete: onDeleteGroup,
+        onToggleVisibility: onToggleGroupVisibility,
       }),
     );
   };
@@ -335,6 +354,7 @@ function BudgetInner(props: BudgetInnerProps) {
         onSave: onSaveCategory,
         onEditNotes: onOpenCategoryNotesModal,
         onDelete: onDeleteCategory,
+        onToggleVisibility: onToggleCategoryVisibility,
         onBudgetAction,
       }),
     );
diff --git a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx
index 025c9a2658a3ca5d45354a8a21a82f1bf39ecb37..0c1d06a008037210d58a81d770559a8c6671c904 100644
--- a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx
+++ b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx
@@ -3,27 +3,24 @@ import React, { type ComponentPropsWithoutRef } from 'react';
 import { useResponsive } from '../../ResponsiveProvider';
 import { theme } from '../../style';
 import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
-import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal';
+import {
+  ModalCloseButton,
+  Modal,
+  ModalTitle,
+  ModalHeader,
+} from '../common/Modal2';
 import { View } from '../common/View';
 import { SectionLabel } from '../forms';
-import { type CommonModalProps } from '../Modals';
 
 type AccountAutocompleteModalProps = {
-  modalProps: CommonModalProps;
   autocompleteProps: ComponentPropsWithoutRef<typeof AccountAutocomplete>;
   onClose: () => void;
 };
 
 export function AccountAutocompleteModal({
-  modalProps,
   autocompleteProps,
   onClose,
 }: AccountAutocompleteModalProps) {
-  const _onClose = () => {
-    modalProps.onClose();
-    onClose?.();
-  };
-
   const { isNarrowWidth } = useResponsive();
   const defaultAutocompleteProps = {
     containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } },
@@ -31,51 +28,57 @@ export function AccountAutocompleteModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle
-          title="Account"
-          getStyle={() => ({ color: theme.menuAutoCompleteText })}
-        />
-      }
+      name="account-autocomplete"
       noAnimation={!isNarrowWidth}
-      showHeader={isNarrowWidth}
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: isNarrowWidth ? '85vh' : 275,
-        backgroundColor: theme.menuAutoCompleteBackground,
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: isNarrowWidth ? '85vh' : 275,
+          backgroundColor: theme.menuAutoCompleteBackground,
+        },
       }}
-      CloseButton={props => (
-        <ModalCloseButton
-          {...props}
-          style={{ color: theme.menuAutoCompleteText }}
-        />
-      )}
     >
-      {() => (
-        <View>
-          {!isNarrowWidth && (
-            <SectionLabel
-              title="Account"
-              style={{
-                alignSelf: 'center',
-                color: theme.menuAutoCompleteText,
-                marginBottom: 10,
-              }}
+      {({ state: { close } }) => (
+        <>
+          {isNarrowWidth && (
+            <ModalHeader
+              title={
+                <ModalTitle
+                  title="Account"
+                  getStyle={() => ({ color: theme.menuAutoCompleteText })}
+                />
+              }
+              rightContent={
+                <ModalCloseButton
+                  onClick={close}
+                  style={{ color: theme.menuAutoCompleteText }}
+                />
+              }
             />
           )}
-          <View style={{ flex: 1 }}>
-            <AccountAutocomplete
-              focused={true}
-              embedded={true}
-              closeOnBlur={false}
-              onClose={_onClose}
-              {...defaultAutocompleteProps}
-              {...autocompleteProps}
-            />
+          <View>
+            {!isNarrowWidth && (
+              <SectionLabel
+                title="Account"
+                style={{
+                  alignSelf: 'center',
+                  color: theme.menuAutoCompleteText,
+                  marginBottom: 10,
+                }}
+              />
+            )}
+            <View style={{ flex: 1 }}>
+              <AccountAutocomplete
+                focused={true}
+                embedded={true}
+                closeOnBlur={false}
+                onClose={close}
+                {...defaultAutocompleteProps}
+                {...autocompleteProps}
+              />
+            </View>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx
index 44fbafb33354ecbbaf1207681c86738ffebf9356..2c4299e2952e51d22577f7b0d0cd192da67e37aa 100644
--- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx
@@ -9,14 +9,17 @@ import { SvgNotesPaper } from '../../icons/v2';
 import { type CSSProperties, styles, theme } from '../../style';
 import { Button } from '../common/Button2';
 import { Menu } from '../common/Menu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Popover } from '../common/Popover';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type AccountMenuModalProps = {
-  modalProps: CommonModalProps;
   accountId: string;
   onSave: (account: AccountEntity) => void;
   onCloseAccount: (accountId: string) => void;
@@ -26,7 +29,6 @@ type AccountMenuModalProps = {
 };
 
 export function AccountMenuModal({
-  modalProps,
   accountId,
   onSave,
   onCloseAccount,
@@ -37,11 +39,6 @@ export function AccountMenuModal({
   const account = useAccount(accountId);
   const originalNotes = useNotes(`account-${accountId}`);
 
-  const _onClose = () => {
-    modalProps?.onClose();
-    onClose?.();
-  };
-
   const onRename = (newName: string) => {
     if (!account) {
       return;
@@ -77,69 +74,84 @@ export function AccountMenuModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle isEditable title={account.name} onTitleUpdate={onRename} />
-      }
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: '45vh',
+      name="account-menu"
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: '45vh',
+        },
       }}
-      leftHeaderContent={
-        <AdditionalAccountMenu
-          account={account}
-          onClose={onCloseAccount}
-          onReopen={onReopenAccount}
-        />
-      }
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <View
-          style={{
-            overflowY: 'auto',
-            flex: 1,
-          }}
-        >
-          <Notes
-            notes={
-              originalNotes && originalNotes.length > 0
-                ? originalNotes
-                : 'No notes'
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            leftContent={
+              <AdditionalAccountMenu
+                account={account}
+                onClose={onCloseAccount}
+                onReopen={onReopenAccount}
+              />
+            }
+            title={
+              <ModalTitle
+                isEditable
+                title={account.name}
+                onTitleUpdate={onRename}
+              />
             }
-            editable={false}
-            focused={false}
-            getStyle={() => ({
-              borderRadius: 6,
-              ...((!originalNotes || originalNotes.length === 0) && {
-                justifySelf: 'center',
-                alignSelf: 'center',
-                color: theme.pageTextSubdued,
-              }),
-            })}
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-        <View
-          style={{
-            flexDirection: 'row',
-            flexWrap: 'wrap',
-            justifyContent: 'space-between',
-            alignContent: 'space-between',
-            paddingTop: 10,
-          }}
-        >
-          <Button style={buttonStyle} onPress={_onEditNotes}>
-            <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} />
-            Edit notes
-          </Button>
-        </View>
-      </View>
+          <View
+            style={{
+              flex: 1,
+              flexDirection: 'column',
+            }}
+          >
+            <View
+              style={{
+                overflowY: 'auto',
+                flex: 1,
+              }}
+            >
+              <Notes
+                notes={
+                  originalNotes && originalNotes.length > 0
+                    ? originalNotes
+                    : 'No notes'
+                }
+                editable={false}
+                focused={false}
+                getStyle={() => ({
+                  borderRadius: 6,
+                  ...((!originalNotes || originalNotes.length === 0) && {
+                    justifySelf: 'center',
+                    alignSelf: 'center',
+                    color: theme.pageTextSubdued,
+                  }),
+                })}
+              />
+            </View>
+            <View
+              style={{
+                flexDirection: 'row',
+                flexWrap: 'wrap',
+                justifyContent: 'space-between',
+                alignContent: 'space-between',
+                paddingTop: 10,
+              }}
+            >
+              <Button style={buttonStyle} onPress={_onEditNotes}>
+                <SvgNotesPaper
+                  width={20}
+                  height={20}
+                  style={{ paddingRight: 5 }}
+                />
+                Edit notes
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/BudgetListModal.tsx b/packages/desktop-client/src/components/modals/BudgetListModal.tsx
index 5d5906dfe75230159ff033e7ce42d953e090bf6f..af44d7960c59486da90b357c2e9b8e869a011099 100644
--- a/packages/desktop-client/src/components/modals/BudgetListModal.tsx
+++ b/packages/desktop-client/src/components/modals/BudgetListModal.tsx
@@ -2,42 +2,42 @@ import React from 'react';
 import { useSelector } from 'react-redux';
 
 import { useLocalPref } from '../../hooks/useLocalPref';
-import { Modal } from '../common/Modal';
+import { Modal, ModalHeader, ModalCloseButton } from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { BudgetList } from '../manager/BudgetList';
-import { type CommonModalProps } from '../Modals';
 
-type BudgetListModalProps = {
-  modalProps: CommonModalProps;
-};
-
-export function BudgetListModal({ modalProps }: BudgetListModalProps) {
+export function BudgetListModal() {
   const [id] = useLocalPref('id');
   const currentFile = useSelector(state =>
     state.budgets.allFiles?.find(f => 'id' in f && f.id === id),
   );
 
   return (
-    <Modal
-      title="Switch Budget File"
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          margin: '20px 0',
-        }}
-      >
-        <Text style={{ fontSize: 17, fontWeight: 400 }}>Switching from:</Text>
-        <Text style={{ fontSize: 17, fontWeight: 700 }}>
-          {currentFile?.name}
-        </Text>
-      </View>
-      <BudgetList showHeader={false} quickSwitchMode={true} />
+    <Modal name="budget-list">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Switch Budget File"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              margin: '20px 0',
+            }}
+          >
+            <Text style={{ fontSize: 17, fontWeight: 400 }}>
+              Switching from:
+            </Text>
+            <Text style={{ fontSize: 17, fontWeight: 700 }}>
+              {currentFile?.name}
+            </Text>
+          </View>
+          <BudgetList showHeader={false} quickSwitchMode={true} />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
index b6b164e52d88fe692489a2a502fded5e86b5e35e..43f943f41ff71069b3079474685019ce4339c262 100644
--- a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx
@@ -3,17 +3,11 @@ import React, { type ComponentPropsWithoutRef } from 'react';
 import { useLocalPref } from '../../hooks/useLocalPref';
 import { type CSSProperties, theme, styles } from '../../style';
 import { Menu } from '../common/Menu';
-import { Modal } from '../common/Modal';
-import { type CommonModalProps } from '../Modals';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 
-type BudgetPageMenuModalProps = ComponentPropsWithoutRef<
-  typeof BudgetPageMenu
-> & {
-  modalProps: CommonModalProps;
-};
+type BudgetPageMenuModalProps = ComponentPropsWithoutRef<typeof BudgetPageMenu>;
 
 export function BudgetPageMenuModal({
-  modalProps,
   onAddCategoryGroup,
   onToggleHiddenCategories,
   onSwitchBudgetFile,
@@ -26,13 +20,21 @@ export function BudgetPageMenuModal({
   };
 
   return (
-    <Modal showHeader focusAfterClose={false} {...modalProps}>
-      <BudgetPageMenu
-        getItemStyle={() => defaultMenuItemStyle}
-        onAddCategoryGroup={onAddCategoryGroup}
-        onToggleHiddenCategories={onToggleHiddenCategories}
-        onSwitchBudgetFile={onSwitchBudgetFile}
-      />
+    <Modal name="budget-page-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            showLogo
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <BudgetPageMenu
+            getItemStyle={() => defaultMenuItemStyle}
+            onAddCategoryGroup={onAddCategoryGroup}
+            onToggleHiddenCategories={onToggleHiddenCategories}
+            onSwitchBudgetFile={onSwitchBudgetFile}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx
index e158b63f13754de893bb0dc381116723cf4a83d0..cfdab70bfb5cd2ab2e4b7c847240f6ecb332933b 100644
--- a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx
+++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx
@@ -5,30 +5,27 @@ import * as monthUtils from 'loot-core/src/shared/months';
 import { useResponsive } from '../../ResponsiveProvider';
 import { theme } from '../../style';
 import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
-import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal';
+import {
+  ModalCloseButton,
+  Modal,
+  ModalTitle,
+  ModalHeader,
+} from '../common/Modal2';
 import { View } from '../common/View';
 import { SectionLabel } from '../forms';
-import { type CommonModalProps } from '../Modals';
 import { NamespaceContext } from '../spreadsheet/NamespaceContext';
 
 type CategoryAutocompleteModalProps = {
-  modalProps: CommonModalProps;
   autocompleteProps: ComponentPropsWithoutRef<typeof CategoryAutocomplete>;
   onClose: () => void;
   month?: string;
 };
 
 export function CategoryAutocompleteModal({
-  modalProps,
   autocompleteProps,
   month,
   onClose,
 }: CategoryAutocompleteModalProps) {
-  const _onClose = () => {
-    modalProps.onClose();
-    onClose?.();
-  };
-
   const { isNarrowWidth } = useResponsive();
 
   const defaultAutocompleteProps = {
@@ -37,56 +34,62 @@ export function CategoryAutocompleteModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle
-          title="Category"
-          getStyle={() => ({ color: theme.menuAutoCompleteText })}
-        />
-      }
+      name="category-autocomplete"
       noAnimation={!isNarrowWidth}
-      showHeader={isNarrowWidth}
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: isNarrowWidth ? '85vh' : 275,
-        backgroundColor: theme.menuAutoCompleteBackground,
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: isNarrowWidth ? '85vh' : 275,
+          backgroundColor: theme.menuAutoCompleteBackground,
+        },
       }}
-      CloseButton={props => (
-        <ModalCloseButton
-          {...props}
-          style={{ color: theme.menuAutoCompleteText }}
-        />
-      )}
     >
-      {() => (
-        <View>
-          {!isNarrowWidth && (
-            <SectionLabel
-              title="Category"
-              style={{
-                alignSelf: 'center',
-                color: theme.menuAutoCompleteText,
-                marginBottom: 10,
-              }}
+      {({ state: { close } }) => (
+        <>
+          {isNarrowWidth && (
+            <ModalHeader
+              title={
+                <ModalTitle
+                  title="Category"
+                  getStyle={() => ({ color: theme.menuAutoCompleteText })}
+                />
+              }
+              rightContent={
+                <ModalCloseButton
+                  onClick={close}
+                  style={{ color: theme.menuAutoCompleteText }}
+                />
+              }
             />
           )}
-          <View style={{ flex: 1 }}>
-            <NamespaceContext.Provider
-              value={month ? monthUtils.sheetForMonth(month) : ''}
-            >
-              <CategoryAutocomplete
-                focused={true}
-                embedded={true}
-                closeOnBlur={false}
-                showSplitOption={false}
-                onClose={_onClose}
-                {...defaultAutocompleteProps}
-                {...autocompleteProps}
+          <View>
+            {!isNarrowWidth && (
+              <SectionLabel
+                title="Category"
+                style={{
+                  alignSelf: 'center',
+                  color: theme.menuAutoCompleteText,
+                  marginBottom: 10,
+                }}
               />
-            </NamespaceContext.Provider>
+            )}
+            <View style={{ flex: 1 }}>
+              <NamespaceContext.Provider
+                value={month ? monthUtils.sheetForMonth(month) : ''}
+              >
+                <CategoryAutocomplete
+                  focused={true}
+                  embedded={true}
+                  closeOnBlur={false}
+                  showSplitOption={false}
+                  onClose={close}
+                  {...defaultAutocompleteProps}
+                  {...autocompleteProps}
+                />
+              </NamespaceContext.Provider>
+            </View>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx
index 4989e6a0c01040bc4a274806e0963077972b1cf0..d155b21d84f1148df98a0c780e6e07e2dda0683b 100644
--- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx
@@ -10,41 +10,40 @@ import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2';
 import { type CSSProperties, styles, theme } from '../../style';
 import { Button } from '../common/Button2';
 import { Menu } from '../common/Menu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Popover } from '../common/Popover';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type CategoryGroupMenuModalProps = {
-  modalProps: CommonModalProps;
   groupId: string;
   onSave: (group: CategoryGroupEntity) => void;
   onAddCategory: (groupId: string, isIncome: boolean) => void;
   onEditNotes: (id: string) => void;
   onSaveNotes: (id: string, notes: string) => void;
   onDelete: (groupId: string) => void;
+  onToggleVisibility: (groupId: string) => void;
   onClose?: () => void;
 };
 
 export function CategoryGroupMenuModal({
-  modalProps,
   groupId,
   onSave,
   onAddCategory,
   onEditNotes,
   onDelete,
+  onToggleVisibility,
   onClose,
 }: CategoryGroupMenuModalProps) {
   const { grouped: categoryGroups } = useCategories();
   const group = categoryGroups.find(g => g.id === groupId);
   const notes = useNotes(group.id);
 
-  const _onClose = () => {
-    modalProps?.onClose();
-    onClose?.();
-  };
-
   const onRename = newName => {
     if (newName !== group.name) {
       onSave?.({
@@ -62,18 +61,14 @@ export function CategoryGroupMenuModal({
     onEditNotes?.(group.id);
   };
 
-  const _onToggleVisibility = () => {
-    onSave?.({
-      ...group,
-      hidden: !!!group.hidden,
-    });
-    _onClose();
-  };
-
   const _onDelete = () => {
     onDelete?.(group.id);
   };
 
+  const _onToggleVisibility = () => {
+    onToggleVisibility?.(group.id);
+  };
+
   const buttonStyle: CSSProperties = {
     ...styles.mediumText,
     height: styles.mobileMinHeight,
@@ -86,70 +81,85 @@ export function CategoryGroupMenuModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle isEditable title={group.name} onTitleUpdate={onRename} />
-      }
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: '45vh',
+      name="category-group-menu"
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: '45vh',
+        },
       }}
-      leftHeaderContent={
-        <AdditionalCategoryGroupMenu
-          group={group}
-          onDelete={_onDelete}
-          onToggleVisibility={_onToggleVisibility}
-        />
-      }
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <View
-          style={{
-            overflowY: 'auto',
-            flex: 1,
-          }}
-        >
-          <Notes
-            notes={notes?.length > 0 ? notes : 'No notes'}
-            editable={false}
-            focused={false}
-            getStyle={() => ({
-              ...styles.mediumText,
-              borderRadius: 6,
-              ...((!notes || notes.length === 0) && {
-                justifySelf: 'center',
-                alignSelf: 'center',
-                color: theme.pageTextSubdued,
-              }),
-            })}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            leftContent={
+              <AdditionalCategoryGroupMenu
+                group={group}
+                onDelete={_onDelete}
+                onToggleVisibility={_onToggleVisibility}
+              />
+            }
+            title={
+              <ModalTitle
+                isEditable
+                title={group.name}
+                onTitleUpdate={onRename}
+              />
+            }
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-        <View
-          style={{
-            flexDirection: 'row',
-            flexWrap: 'wrap',
-            justifyContent: 'space-between',
-            alignContent: 'space-between',
-            paddingTop: 10,
-          }}
-        >
-          <Button style={buttonStyle} onPress={_onAddCategory}>
-            <SvgAdd width={17} height={17} style={{ paddingRight: 5 }} />
-            Add category
-          </Button>
-          <Button style={buttonStyle} onPress={_onEditNotes}>
-            <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} />
-            Edit notes
-          </Button>
-        </View>
-      </View>
+          <View
+            style={{
+              flex: 1,
+              flexDirection: 'column',
+            }}
+          >
+            <View
+              style={{
+                overflowY: 'auto',
+                flex: 1,
+              }}
+            >
+              <Notes
+                notes={notes?.length > 0 ? notes : 'No notes'}
+                editable={false}
+                focused={false}
+                getStyle={() => ({
+                  ...styles.mediumText,
+                  borderRadius: 6,
+                  ...((!notes || notes.length === 0) && {
+                    justifySelf: 'center',
+                    alignSelf: 'center',
+                    color: theme.pageTextSubdued,
+                  }),
+                })}
+              />
+            </View>
+            <View
+              style={{
+                flexDirection: 'row',
+                flexWrap: 'wrap',
+                justifyContent: 'space-between',
+                alignContent: 'space-between',
+                paddingTop: 10,
+              }}
+            >
+              <Button style={buttonStyle} onPress={_onAddCategory}>
+                <SvgAdd width={17} height={17} style={{ paddingRight: 5 }} />
+                Add category
+              </Button>
+              <Button style={buttonStyle} onPress={_onEditNotes}>
+                <SvgNotesPaper
+                  width={20}
+                  height={20}
+                  style={{ paddingRight: 5 }}
+                />
+                Edit notes
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx
index 9451de7cb4a359c0f525ef5d26698d2f536bcd1b..c6991962751f6bcc960fda9721f7f86aab529ddd 100644
--- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx
@@ -11,36 +11,36 @@ import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2';
 import { type CSSProperties, styles, theme } from '../../style';
 import { Button } from '../common/Button2';
 import { Menu } from '../common/Menu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Popover } from '../common/Popover';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type CategoryMenuModalProps = {
-  modalProps: CommonModalProps;
   categoryId: string;
   onSave: (category: CategoryEntity) => void;
-  onEditNotes: (id: string) => void;
+  onEditNotes: (categoryId: string) => void;
   onDelete: (categoryId: string) => void;
+  onToggleVisibility: (categoryId: string) => void;
   onClose?: () => void;
 };
 
 export function CategoryMenuModal({
-  modalProps,
   categoryId,
   onSave,
   onEditNotes,
   onDelete,
+  onToggleVisibility,
   onClose,
 }: CategoryMenuModalProps) {
   const category = useCategory(categoryId);
   const categoryGroup = useCategoryGroup(category?.cat_group);
   const originalNotes = useNotes(category.id);
-  const _onClose = () => {
-    modalProps?.onClose();
-    onClose?.();
-  };
 
   const onRename = newName => {
     if (newName !== category.name) {
@@ -52,11 +52,7 @@ export function CategoryMenuModal({
   };
 
   const _onToggleVisibility = () => {
-    onSave?.({
-      ...category,
-      hidden: !category.hidden,
-    });
-    _onClose();
+    onToggleVisibility?.(category.id);
   };
 
   const _onEditNotes = () => {
@@ -77,66 +73,79 @@ export function CategoryMenuModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle isEditable title={category.name} onTitleUpdate={onRename} />
-      }
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: '45vh',
+      name="category-menu"
+      onClose={onClose}
+      containerProps={{
+        style: { height: '45vh' },
       }}
-      leftHeaderContent={
-        <AdditionalCategoryMenu
-          category={category}
-          categoryGroup={categoryGroup}
-          onDelete={_onDelete}
-          onToggleVisibility={_onToggleVisibility}
-        />
-      }
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <View
-          style={{
-            overflowY: 'auto',
-            flex: 1,
-          }}
-        >
-          <Notes
-            notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
-            editable={false}
-            focused={false}
-            getStyle={() => ({
-              borderRadius: 6,
-              ...((!originalNotes || originalNotes.length === 0) && {
-                justifySelf: 'center',
-                alignSelf: 'center',
-                color: theme.pageTextSubdued,
-              }),
-            })}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            leftContent={
+              <AdditionalCategoryMenu
+                category={category}
+                categoryGroup={categoryGroup}
+                onDelete={_onDelete}
+                onToggleVisibility={_onToggleVisibility}
+              />
+            }
+            title={
+              <ModalTitle
+                isEditable
+                title={category.name}
+                onTitleUpdate={onRename}
+              />
+            }
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-        <View
-          style={{
-            flexDirection: 'row',
-            flexWrap: 'wrap',
-            justifyContent: 'space-between',
-            alignContent: 'space-between',
-            paddingTop: 10,
-          }}
-        >
-          <Button style={buttonStyle} onPress={_onEditNotes}>
-            <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} />
-            Edit notes
-          </Button>
-        </View>
-      </View>
+          <View
+            style={{
+              flex: 1,
+              flexDirection: 'column',
+            }}
+          >
+            <View
+              style={{
+                overflowY: 'auto',
+                flex: 1,
+              }}
+            >
+              <Notes
+                notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
+                editable={false}
+                focused={false}
+                getStyle={() => ({
+                  borderRadius: 6,
+                  ...((!originalNotes || originalNotes.length === 0) && {
+                    justifySelf: 'center',
+                    alignSelf: 'center',
+                    color: theme.pageTextSubdued,
+                  }),
+                })}
+              />
+            </View>
+            <View
+              style={{
+                flexDirection: 'row',
+                flexWrap: 'wrap',
+                justifyContent: 'space-between',
+                alignContent: 'space-between',
+                paddingTop: 10,
+              }}
+            >
+              <Button style={buttonStyle} onPress={_onEditNotes}>
+                <SvgNotesPaper
+                  width={20}
+                  height={20}
+                  style={{ paddingRight: 5 }}
+                />
+                Edit notes
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx
index 93c87019fbdbfb19df4a18feef3daf118ce64dec..c1668a70105217f8da6455c7f9436b4ac85b535b 100644
--- a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx
+++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx
@@ -20,11 +20,10 @@ import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
 import { Button } from '../common/Button2';
 import { FormError } from '../common/FormError';
 import { Link } from '../common/Link';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 function needsCategory(
   account: AccountEntity,
@@ -43,14 +42,12 @@ type CloseAccountModalProps = {
   account: AccountEntity;
   balance: number;
   canDelete: boolean;
-  modalProps: CommonModalProps;
 };
 
 export function CloseAccountModal({
   account,
   balance,
   canDelete,
-  modalProps,
 }: CloseAccountModalProps) {
   const accounts = useAccounts().filter(a => a.closed === 0);
   const { grouped: categoryGroups, list: categories } = useCategories();
@@ -103,163 +100,173 @@ export function CloseAccountModal({
       dispatch(
         closeAccount(account.id, transferAccountId || null, categoryId || null),
       );
-      modalProps.onClose();
     }
   };
 
   return (
     <Modal
-      title="Close Account"
-      {...modalProps}
-      style={{ flex: 0 }}
-      loading={loading}
+      name="close-account"
+      isLoading={loading}
+      containerProps={{ style: { width: '30vw' } }}
     >
-      {() => (
-        <View>
-          <Paragraph>
-            Are you sure you want to close <strong>{account.name}</strong>?{' '}
-            {canDelete ? (
-              <span>
-                This account has no transactions so it will be permanently
-                deleted.
-              </span>
-            ) : (
-              <span>
-                This account has transactions so we can’t permanently delete it.
-              </span>
-            )}
-          </Paragraph>
-          <Form onSubmit={onSubmit}>
-            {balance !== 0 && (
-              <View>
-                <Paragraph>
-                  This account has a balance of{' '}
-                  <strong>{integerToCurrency(balance)}</strong>. To close this
-                  account, select a different account to transfer this balance
-                  to:
-                </Paragraph>
-
-                <View style={{ marginBottom: 15 }}>
-                  <AccountAutocomplete
-                    includeClosedAccounts={false}
-                    value={transferAccountId}
-                    inputProps={{
-                      placeholder: 'Select account...',
-                      ...(isNarrowWidth && {
-                        value: transferAccount?.name || '',
-                        style: {
-                          ...narrowStyle,
-                        },
-                        onClick: () => {
-                          dispatch(
-                            pushModal('account-autocomplete', {
-                              includeClosedAccounts: false,
-                              onSelect: onSelectAccount,
-                            }),
-                          );
-                        },
-                      }),
-                    }}
-                    onSelect={onSelectAccount}
-                  />
-                </View>
-
-                {transferError && (
-                  <FormError style={{ marginBottom: 15 }}>
-                    Transfer is required
-                  </FormError>
-                )}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Close Account"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <Paragraph>
+              Are you sure you want to close <strong>{account.name}</strong>?{' '}
+              {canDelete ? (
+                <span>
+                  This account has no transactions so it will be permanently
+                  deleted.
+                </span>
+              ) : (
+                <span>
+                  This account has transactions so we can’t permanently delete
+                  it.
+                </span>
+              )}
+            </Paragraph>
+            <Form
+              onSubmit={e => {
+                onSubmit(e);
+                close();
+              }}
+            >
+              {balance !== 0 && (
+                <View>
+                  <Paragraph>
+                    This account has a balance of{' '}
+                    <strong>{integerToCurrency(balance)}</strong>. To close this
+                    account, select a different account to transfer this balance
+                    to:
+                  </Paragraph>
 
-                {needsCategory(account, transferAccountId, accounts) && (
                   <View style={{ marginBottom: 15 }}>
-                    <Paragraph>
-                      Since you are transferring the balance from a budgeted
-                      account to an off-budget account, this transaction must be
-                      categorized. Select a category:
-                    </Paragraph>
-
-                    <CategoryAutocomplete
-                      categoryGroups={categoryGroups}
-                      value={categoryId}
+                    <AccountAutocomplete
+                      includeClosedAccounts={false}
+                      value={transferAccountId}
                       inputProps={{
-                        placeholder: 'Select category...',
+                        placeholder: 'Select account...',
                         ...(isNarrowWidth && {
-                          value: category?.name || '',
+                          value: transferAccount?.name || '',
                           style: {
                             ...narrowStyle,
                           },
                           onClick: () => {
                             dispatch(
-                              pushModal('category-autocomplete', {
-                                categoryGroups,
-                                showHiddenCategories: true,
-                                onSelect: onSelectCategory,
+                              pushModal('account-autocomplete', {
+                                includeClosedAccounts: false,
+                                onSelect: onSelectAccount,
                               }),
                             );
                           },
                         }),
                       }}
-                      onSelect={onSelectCategory}
+                      onSelect={onSelectAccount}
                     />
-
-                    {categoryError && (
-                      <FormError>Category is required</FormError>
-                    )}
                   </View>
-                )}
-              </View>
-            )}
 
-            {!canDelete && (
-              <View style={{ marginBottom: 15 }}>
-                <Text style={{ fontSize: 12 }}>
-                  You can also{' '}
-                  <Link
-                    variant="text"
-                    onClick={() => {
-                      setLoading(true);
+                  {transferError && (
+                    <FormError style={{ marginBottom: 15 }}>
+                      Transfer is required
+                    </FormError>
+                  )}
 
-                      dispatch(forceCloseAccount(account.id));
-                      modalProps.onClose();
-                    }}
-                    style={{ color: theme.errorText }}
-                  >
-                    force close
-                  </Link>{' '}
-                  the account which will delete it and all its transactions
-                  permanently. Doing so may change your budget unexpectedly
-                  since money in it may vanish.
-                </Text>
-              </View>
-            )}
+                  {needsCategory(account, transferAccountId, accounts) && (
+                    <View style={{ marginBottom: 15 }}>
+                      <Paragraph>
+                        Since you are transferring the balance from a budgeted
+                        account to an off-budget account, this transaction must
+                        be categorized. Select a category:
+                      </Paragraph>
 
-            <View
-              style={{
-                flexDirection: 'row',
-                justifyContent: 'flex-end',
-              }}
-            >
-              <Button
-                style={{
-                  marginRight: 10,
-                  height: isNarrowWidth ? styles.mobileMinHeight : undefined,
-                }}
-                onPress={modalProps.onClose}
-              >
-                Cancel
-              </Button>
-              <Button
-                type="submit"
-                variant="primary"
+                      <CategoryAutocomplete
+                        categoryGroups={categoryGroups}
+                        value={categoryId}
+                        inputProps={{
+                          placeholder: 'Select category...',
+                          ...(isNarrowWidth && {
+                            value: category?.name || '',
+                            style: {
+                              ...narrowStyle,
+                            },
+                            onClick: () => {
+                              dispatch(
+                                pushModal('category-autocomplete', {
+                                  categoryGroups,
+                                  showHiddenCategories: true,
+                                  onSelect: onSelectCategory,
+                                }),
+                              );
+                            },
+                          }),
+                        }}
+                        onSelect={onSelectCategory}
+                      />
+
+                      {categoryError && (
+                        <FormError>Category is required</FormError>
+                      )}
+                    </View>
+                  )}
+                </View>
+              )}
+
+              {!canDelete && (
+                <View style={{ marginBottom: 15 }}>
+                  <Text style={{ fontSize: 12 }}>
+                    You can also{' '}
+                    <Link
+                      variant="text"
+                      onClick={() => {
+                        setLoading(true);
+
+                        dispatch(forceCloseAccount(account.id));
+                        close();
+                      }}
+                      style={{ color: theme.errorText }}
+                    >
+                      force close
+                    </Link>{' '}
+                    the account which will delete it and all its transactions
+                    permanently. Doing so may change your budget unexpectedly
+                    since money in it may vanish.
+                  </Text>
+                </View>
+              )}
+
+              <View
                 style={{
-                  height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  flexDirection: 'row',
+                  justifyContent: 'flex-end',
                 }}
               >
-                Close Account
-              </Button>
-            </View>
-          </Form>
-        </View>
+                <Button
+                  style={{
+                    marginRight: 10,
+                    height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  }}
+                  onPress={close}
+                >
+                  Cancel
+                </Button>
+                <Button
+                  type="submit"
+                  variant="primary"
+                  style={{
+                    height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  }}
+                >
+                  Close Account
+                </Button>
+              </View>
+            </Form>
+          </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
index a3726e0b98b109d94087aa86d32d5b85150e75b6..7713a6a09ffdbbe8f421ae6d2bdd1624a3a4dc00 100644
--- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
+++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx
@@ -6,20 +6,17 @@ import { theme } from '../../style';
 import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete';
 import { Block } from '../common/Block';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type ConfirmCategoryDeleteProps = {
-  modalProps: CommonModalProps;
   category: string;
   group: string;
   onDelete: (categoryId: string) => void;
 };
 
 export function ConfirmCategoryDelete({
-  modalProps,
   group: groupId,
   category: categoryId,
   onDelete,
@@ -56,82 +53,92 @@ export function ConfirmCategoryDelete({
   const isIncome = !!(category || group).is_income;
 
   return (
-    <Modal title="Confirm Delete" {...modalProps} style={{ flex: 0 }}>
-      {() => (
-        <View style={{ lineHeight: 1.5 }}>
-          {group ? (
-            <Block>
-              Categories in the group <strong>{group.name}</strong> are used by
-              existing transaction
-              {!isIncome &&
-                ' or it has a positive leftover balance currently'}.{' '}
-              <strong>Are you sure you want to delete it?</strong> If so, you
-              must select another category to transfer existing transactions and
-              balance to.
-            </Block>
-          ) : (
-            <Block>
-              <strong>{category.name}</strong> is used by existing transactions
-              {!isIncome &&
-                ' or it has a positive leftover balance currently'}.{' '}
-              <strong>Are you sure you want to delete it?</strong> If so, you
-              must select another category to transfer existing transactions and
-              balance to.
-            </Block>
-          )}
+    <Modal
+      name="confirm-category-delete"
+      containerProps={{ style: { width: '30vw' } }}
+    >
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Confirm Delete"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ lineHeight: 1.5 }}>
+            {group ? (
+              <Block>
+                Categories in the group <strong>{group.name}</strong> are used
+                by existing transaction
+                {!isIncome &&
+                  ' or it has a positive leftover balance currently'}
+                . <strong>Are you sure you want to delete it?</strong> If so,
+                you must select another category to transfer existing
+                transactions and balance to.
+              </Block>
+            ) : (
+              <Block>
+                <strong>{category.name}</strong> is used by existing
+                transactions
+                {!isIncome &&
+                  ' or it has a positive leftover balance currently'}
+                . <strong>Are you sure you want to delete it?</strong> If so,
+                you must select another category to transfer existing
+                transactions and balance to.
+              </Block>
+            )}
 
-          {error && renderError(error)}
+            {error && renderError(error)}
 
-          <View
-            style={{
-              marginTop: 20,
-              flexDirection: 'row',
-              justifyContent: 'flex-start',
-              alignItems: 'center',
-            }}
-          >
-            <Text>Transfer to:</Text>
+            <View
+              style={{
+                marginTop: 20,
+                flexDirection: 'row',
+                justifyContent: 'flex-start',
+                alignItems: 'center',
+              }}
+            >
+              <Text>Transfer to:</Text>
+
+              <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}>
+                <CategoryAutocomplete
+                  categoryGroups={
+                    group
+                      ? categoryGroups.filter(
+                          g => g.id !== group.id && !!g.is_income === isIncome,
+                        )
+                      : categoryGroups
+                          .filter(g => !!g.is_income === isIncome)
+                          .map(g => ({
+                            ...g,
+                            categories: g.categories.filter(
+                              c => c.id !== category.id,
+                            ),
+                          }))
+                  }
+                  value={transferCategory}
+                  inputProps={{
+                    placeholder: 'Select category...',
+                  }}
+                  onSelect={category => setTransferCategory(category)}
+                  showHiddenCategories={true}
+                />
+              </View>
 
-            <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}>
-              <CategoryAutocomplete
-                categoryGroups={
-                  group
-                    ? categoryGroups.filter(
-                        g => g.id !== group.id && !!g.is_income === isIncome,
-                      )
-                    : categoryGroups
-                        .filter(g => !!g.is_income === isIncome)
-                        .map(g => ({
-                          ...g,
-                          categories: g.categories.filter(
-                            c => c.id !== category.id,
-                          ),
-                        }))
-                }
-                value={transferCategory}
-                inputProps={{
-                  placeholder: 'Select category...',
+              <Button
+                variant="primary"
+                onPress={() => {
+                  if (!transferCategory) {
+                    setError('required-transfer');
+                  } else {
+                    onDelete(transferCategory);
+                    close();
+                  }
                 }}
-                onSelect={category => setTransferCategory(category)}
-                showHiddenCategories={true}
-              />
+              >
+                Delete
+              </Button>
             </View>
-
-            <Button
-              variant="primary"
-              onPress={() => {
-                if (!transferCategory) {
-                  setError('required-transfer');
-                } else {
-                  onDelete(transferCategory);
-                  modalProps.onClose();
-                }
-              }}
-            >
-              Delete
-            </Button>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx
index f66ea339c43e2dc76735a814d0d0214274812d74..3c1ceaba0255bafca5435dc4497c4ce068fa8693 100644
--- a/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx
+++ b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx
@@ -3,18 +3,15 @@ import React from 'react';
 import { useResponsive } from '../../ResponsiveProvider';
 import { styles } from '../../style';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type ConfirmTransactionDeleteProps = {
-  modalProps: CommonModalProps;
   onConfirm: () => void;
 };
 
 export function ConfirmTransactionDelete({
-  modalProps,
   onConfirm,
 }: ConfirmTransactionDeleteProps) {
   const { isNarrowWidth } = useResponsive();
@@ -25,36 +22,46 @@ export function ConfirmTransactionDelete({
     : {};
 
   return (
-    <Modal title="Confirm Delete" {...modalProps}>
-      <View style={{ lineHeight: 1.5 }}>
-        <Paragraph>Are you sure you want to delete the transaction?</Paragraph>
-        <View
-          style={{
-            flexDirection: 'row',
-            justifyContent: 'flex-end',
-          }}
-        >
-          <Button
-            style={{
-              marginRight: 10,
-              ...narrowButtonStyle,
-            }}
-            onPress={modalProps.onClose}
-          >
-            Cancel
-          </Button>
-          <Button
-            variant="primary"
-            style={narrowButtonStyle}
-            onPress={() => {
-              onConfirm();
-              modalProps.onClose();
-            }}
-          >
-            Delete
-          </Button>
-        </View>
-      </View>
+    <Modal name="confirm-transaction-delete">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Confirm Delete"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ lineHeight: 1.5 }}>
+            <Paragraph>
+              Are you sure you want to delete the transaction?
+            </Paragraph>
+            <View
+              style={{
+                flexDirection: 'row',
+                justifyContent: 'flex-end',
+              }}
+            >
+              <Button
+                style={{
+                  marginRight: 10,
+                  ...narrowButtonStyle,
+                }}
+                onPress={close}
+              >
+                Cancel
+              </Button>
+              <Button
+                variant="primary"
+                style={narrowButtonStyle}
+                onPress={() => {
+                  onConfirm();
+                  close();
+                }}
+              >
+                Delete
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx
index 0d88859c5eed296c0b0392e78b8c3b8b6c0898a7..6b445bda669085eec147f9d2289130fdf192d071 100644
--- a/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx
+++ b/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx
@@ -3,97 +3,105 @@ import React from 'react';
 
 import { Block } from '../common/Block';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type ConfirmTransactionEditProps = {
-  modalProps: Partial<CommonModalProps>;
   onCancel?: () => void;
   onConfirm: () => void;
   confirmReason: string;
 };
 
 export function ConfirmTransactionEdit({
-  modalProps,
   onCancel,
   onConfirm,
   confirmReason,
 }: ConfirmTransactionEditProps) {
   return (
-    <Modal title="Reconciled Transaction" {...modalProps} style={{ flex: 0 }}>
-      {() => (
-        <View style={{ lineHeight: 1.5 }}>
-          {confirmReason === 'batchDeleteWithReconciled' ? (
-            <Block>
-              Deleting reconciled transactions may bring your reconciliation out
-              of balance.
-            </Block>
-          ) : confirmReason === 'batchEditWithReconciled' ? (
-            <Block>
-              Editing reconciled transactions may bring your reconciliation out
-              of balance.
-            </Block>
-          ) : confirmReason === 'batchDuplicateWithReconciled' ? (
-            <Block>
-              Duplicating reconciled transactions may bring your reconciliation
-              out of balance.
-            </Block>
-          ) : confirmReason === 'editReconciled' ? (
-            <Block>
-              Saving your changes to this reconciled transaction may bring your
-              reconciliation out of balance.
-            </Block>
-          ) : confirmReason === 'unlockReconciled' ? (
-            <Block>
-              Unlocking this transaction means you won‘t be warned about changes
-              that can impact your reconciled balance. (Changes to amount,
-              account, payee, etc).
-            </Block>
-          ) : confirmReason === 'deleteReconciled' ? (
-            <Block>
-              Deleting this reconciled transaction may bring your reconciliation
-              out of balance.
-            </Block>
-          ) : (
-            <Block>Are you sure you want to edit this transaction?</Block>
-          )}
+    <Modal
+      name="confirm-transaction-edit"
+      containerProps={{ style: { width: '30vw' } }}
+    >
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Reconciled Transaction"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ lineHeight: 1.5 }}>
+            {confirmReason === 'batchDeleteWithReconciled' ? (
+              <Block>
+                Deleting reconciled transactions may bring your reconciliation
+                out of balance.
+              </Block>
+            ) : confirmReason === 'batchEditWithReconciled' ? (
+              <Block>
+                Editing reconciled transactions may bring your reconciliation
+                out of balance.
+              </Block>
+            ) : confirmReason === 'batchDuplicateWithReconciled' ? (
+              <Block>
+                Duplicating reconciled transactions may bring your
+                reconciliation out of balance.
+              </Block>
+            ) : confirmReason === 'editReconciled' ? (
+              <Block>
+                Saving your changes to this reconciled transaction may bring
+                your reconciliation out of balance.
+              </Block>
+            ) : confirmReason === 'unlockReconciled' ? (
+              <Block>
+                Unlocking this transaction means you won‘t be warned about
+                changes that can impact your reconciled balance. (Changes to
+                amount, account, payee, etc).
+              </Block>
+            ) : confirmReason === 'deleteReconciled' ? (
+              <Block>
+                Deleting this reconciled transaction may bring your
+                reconciliation out of balance.
+              </Block>
+            ) : (
+              <Block>Are you sure you want to edit this transaction?</Block>
+            )}
 
-          <View
-            style={{
-              marginTop: 20,
-              flexDirection: 'row',
-              justifyContent: 'flex-start',
-              alignItems: 'center',
-            }}
-          >
             <View
               style={{
+                marginTop: 20,
                 flexDirection: 'row',
-                justifyContent: 'flex-end',
+                justifyContent: 'flex-start',
+                alignItems: 'center',
               }}
             >
-              <Button
-                style={{ marginRight: 10 }}
-                onPress={() => {
-                  modalProps.onClose();
-                  onCancel();
+              <View
+                style={{
+                  flexDirection: 'row',
+                  justifyContent: 'flex-end',
                 }}
               >
-                Cancel
-              </Button>
-              <Button
-                variant="primary"
-                onPress={() => {
-                  modalProps.onClose();
-                  onConfirm();
-                }}
-              >
-                Confirm
-              </Button>
+                <Button
+                  aria-label="Cancel"
+                  style={{ marginRight: 10 }}
+                  onPress={() => {
+                    close();
+                    onCancel();
+                  }}
+                >
+                  Cancel
+                </Button>
+                <Button
+                  aria-label="Confirm"
+                  variant="primary"
+                  onPress={() => {
+                    close();
+                    onConfirm();
+                  }}
+                >
+                  Confirm
+                </Button>
+              </View>
             </View>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx
index 88fca3fe758af16a8791e53dc151d91baba6f47e..e3d5fb53595935c4ecfcf93cd0fed864028fa59d 100644
--- a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx
+++ b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx
@@ -1,55 +1,61 @@
 import React from 'react';
 
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type ConfirmUnlinkAccountProps = {
-  modalProps: CommonModalProps;
   accountName: string;
   onUnlink: () => void;
 };
 
 export function ConfirmUnlinkAccount({
-  modalProps,
   accountName,
   onUnlink,
 }: ConfirmUnlinkAccountProps) {
   return (
-    <Modal title="Confirm Unlink" {...modalProps} style={{ flex: 0 }}>
-      {() => (
-        <View style={{ lineHeight: 1.5 }}>
-          <Paragraph>
-            Are you sure you want to unlink <strong>{accountName}</strong>?
-          </Paragraph>
+    <Modal
+      name="confirm-unlink-account"
+      containerProps={{ style: { width: '30vw' } }}
+    >
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Confirm Unlink"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ lineHeight: 1.5 }}>
+            <Paragraph>
+              Are you sure you want to unlink <strong>{accountName}</strong>?
+            </Paragraph>
 
-          <Paragraph>
-            Transactions will no longer be synchronized with this account and
-            must be manually entered.
-          </Paragraph>
+            <Paragraph>
+              Transactions will no longer be synchronized with this account and
+              must be manually entered.
+            </Paragraph>
 
-          <View
-            style={{
-              flexDirection: 'row',
-              justifyContent: 'flex-end',
-            }}
-          >
-            <Button style={{ marginRight: 10 }} onPress={modalProps.onClose}>
-              Cancel
-            </Button>
-            <Button
-              variant="primary"
-              onPress={() => {
-                onUnlink();
-                modalProps.onClose();
+            <View
+              style={{
+                flexDirection: 'row',
+                justifyContent: 'flex-end',
               }}
             >
-              Unlink
-            </Button>
+              <Button style={{ marginRight: 10 }} onPress={close}>
+                Cancel
+              </Button>
+              <Button
+                variant="primary"
+                onPress={() => {
+                  onUnlink();
+                  close();
+                }}
+              >
+                Unlink
+              </Button>
+            </View>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/CoverModal.tsx b/packages/desktop-client/src/components/modals/CoverModal.tsx
index 490f0f4f0575cd9038fd28479adfb05c0146b51e..fbf9e8f14104dd408d9cc616b4ae1eeb70430b94 100644
--- a/packages/desktop-client/src/components/modals/CoverModal.tsx
+++ b/packages/desktop-client/src/components/modals/CoverModal.tsx
@@ -8,13 +8,11 @@ import { useInitialMount } from '../../hooks/useInitialMount';
 import { styles } from '../../style';
 import { addToBeBudgetedGroup } from '../budget/util';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
 import { FieldLabel, TapField } from '../mobile/MobileForms';
-import { type CommonModalProps } from '../Modals';
 
 type CoverModalProps = {
-  modalProps: CommonModalProps;
   title: string;
   month: string;
   showToBeBudgeted?: boolean;
@@ -22,7 +20,6 @@ type CoverModalProps = {
 };
 
 export function CoverModal({
-  modalProps,
   title,
   month,
   showToBeBudgeted = true,
@@ -57,8 +54,6 @@ export function CoverModal({
     if (categoryId) {
       onSubmit?.(categoryId);
     }
-
-    modalProps.onClose();
   };
 
   const initialMount = useInitialMount();
@@ -72,31 +67,42 @@ export function CoverModal({
   const fromCategory = categories.find(c => c.id === fromCategoryId);
 
   return (
-    <Modal title={title} showHeader focusAfterClose={false} {...modalProps}>
-      <View>
-        <FieldLabel title="Cover from category:" />
-        <TapField value={fromCategory?.name} onClick={onCategoryClick} />
-      </View>
+    <Modal name="cover">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={title}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <FieldLabel title="Cover from category:" />
+            <TapField value={fromCategory?.name} onClick={onCategoryClick} />
+          </View>
 
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          paddingTop: 10,
-        }}
-      >
-        <Button
-          variant="primary"
-          style={{
-            height: styles.mobileMinHeight,
-            marginLeft: styles.mobileEditingPadding,
-            marginRight: styles.mobileEditingPadding,
-          }}
-          onPress={() => _onSubmit(fromCategoryId)}
-        >
-          Transfer
-        </Button>
-      </View>
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              paddingTop: 10,
+            }}
+          >
+            <Button
+              variant="primary"
+              style={{
+                height: styles.mobileMinHeight,
+                marginLeft: styles.mobileEditingPadding,
+                marginRight: styles.mobileEditingPadding,
+              }}
+              onPress={() => {
+                _onSubmit(fromCategoryId);
+                close();
+              }}
+            >
+              Transfer
+            </Button>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx
index b9cd815ef4b822de5f0122cf780169fb583cb09f..c23e67aaf2e3d001502f70e9e610c807fd0c58e2 100644
--- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx
+++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx
@@ -14,21 +14,18 @@ import { theme } from '../../style';
 import { Button, ButtonWithLoading } from '../common/Button2';
 import { Link } from '../common/Link';
 import { Menu } from '../common/Menu';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Popover } from '../common/Popover';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type CreateAccountProps = {
-  modalProps: CommonModalProps;
   syncServerStatus: SyncServerStatus;
   upgradingAccountId?: string;
 };
 
 export function CreateAccountModal({
-  modalProps,
   syncServerStatus,
   upgradingAccountId,
 }: CreateAccountProps) {
@@ -179,204 +176,211 @@ export function CreateAccountModal({
   const simpleFinSyncFeatureFlag = useFeatureFlag('simpleFinSync');
 
   return (
-    <Modal title={title} {...modalProps}>
-      {() => (
-        <View style={{ maxWidth: 500, gap: 30, color: theme.pageText }}>
-          {upgradingAccountId == null && (
-            <View style={{ gap: 10 }}>
-              <Button
-                variant="primary"
-                style={{
-                  padding: '10px 0',
-                  fontSize: 15,
-                  fontWeight: 600,
-                }}
-                onPress={onCreateLocalAccount}
-              >
-                Create local account
-              </Button>
-              <View style={{ lineHeight: '1.4em', fontSize: 15 }}>
-                <Text>
-                  <strong>Create a local account</strong> if you want to add
-                  transactions manually. You can also{' '}
-                  <Link
-                    variant="external"
-                    to="https://actualbudget.org/docs/transactions/importing"
-                    linkColor="muted"
-                  >
-                    import QIF/OFX/QFX files into a local account
-                  </Link>
-                  .
-                </Text>
-              </View>
-            </View>
-          )}
-          <View style={{ gap: 10 }}>
-            {syncServerStatus === 'online' ? (
-              <>
-                <View
+    <Modal name="add-account">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={title}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ maxWidth: 500, gap: 30, color: theme.pageText }}>
+            {upgradingAccountId == null && (
+              <View style={{ gap: 10 }}>
+                <Button
+                  variant="primary"
                   style={{
-                    flexDirection: 'row',
-                    gap: 10,
-                    alignItems: 'center',
+                    padding: '10px 0',
+                    fontSize: 15,
+                    fontWeight: 600,
                   }}
+                  onPress={onCreateLocalAccount}
                 >
-                  <ButtonWithLoading
-                    isDisabled={syncServerStatus !== 'online'}
+                  Create local account
+                </Button>
+                <View style={{ lineHeight: '1.4em', fontSize: 15 }}>
+                  <Text>
+                    <strong>Create a local account</strong> if you want to add
+                    transactions manually. You can also{' '}
+                    <Link
+                      variant="external"
+                      to="https://actualbudget.org/docs/transactions/importing"
+                      linkColor="muted"
+                    >
+                      import QIF/OFX/QFX files into a local account
+                    </Link>
+                    .
+                  </Text>
+                </View>
+              </View>
+            )}
+            <View style={{ gap: 10 }}>
+              {syncServerStatus === 'online' ? (
+                <>
+                  <View
                     style={{
-                      padding: '10px 0',
-                      fontSize: 15,
-                      fontWeight: 600,
-                      flex: 1,
+                      flexDirection: 'row',
+                      gap: 10,
+                      alignItems: 'center',
                     }}
-                    onPress={onConnectGoCardless}
                   >
-                    {isGoCardlessSetupComplete
-                      ? 'Link bank account with GoCardless'
-                      : 'Set up GoCardless for bank sync'}
-                  </ButtonWithLoading>
-                  {isGoCardlessSetupComplete && (
-                    <>
-                      <Button
-                        ref={triggerRef}
-                        variant="bare"
-                        onPress={() => setGoCardlessMenuOpen(true)}
-                        aria-label="GoCardless menu"
-                      >
-                        <SvgDotsHorizontalTriple
-                          width={15}
-                          height={15}
-                          style={{ transform: 'rotateZ(90deg)' }}
-                        />
-                      </Button>
-
-                      <Popover
-                        triggerRef={triggerRef}
-                        isOpen={menuGoCardlessOpen}
-                        onOpenChange={() => setGoCardlessMenuOpen(false)}
-                      >
-                        <Menu
-                          onMenuSelect={item => {
-                            if (item === 'reconfigure') {
-                              onGoCardlessReset();
-                            }
-                          }}
-                          items={[
-                            {
-                              name: 'reconfigure',
-                              text: 'Reset GoCardless credentials',
-                            },
-                          ]}
-                        />
-                      </Popover>
-                    </>
-                  )}
-                </View>
-                <Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
-                  <strong>
-                    Link a <em>European</em> bank account
-                  </strong>{' '}
-                  to automatically download transactions. GoCardless provides
-                  reliable, up-to-date information from hundreds of banks.
-                </Text>
-                {simpleFinSyncFeatureFlag === true && (
-                  <>
-                    <View
+                    <ButtonWithLoading
+                      isDisabled={syncServerStatus !== 'online'}
                       style={{
-                        flexDirection: 'row',
-                        gap: 10,
-                        marginTop: '18px',
-                        alignItems: 'center',
+                        padding: '10px 0',
+                        fontSize: 15,
+                        fontWeight: 600,
+                        flex: 1,
                       }}
+                      onPress={onConnectGoCardless}
                     >
-                      <ButtonWithLoading
-                        isDisabled={syncServerStatus !== 'online'}
-                        isLoading={loadingSimpleFinAccounts}
+                      {isGoCardlessSetupComplete
+                        ? 'Link bank account with GoCardless'
+                        : 'Set up GoCardless for bank sync'}
+                    </ButtonWithLoading>
+                    {isGoCardlessSetupComplete && (
+                      <>
+                        <Button
+                          ref={triggerRef}
+                          variant="bare"
+                          onPress={() => setGoCardlessMenuOpen(true)}
+                          aria-label="GoCardless menu"
+                        >
+                          <SvgDotsHorizontalTriple
+                            width={15}
+                            height={15}
+                            style={{ transform: 'rotateZ(90deg)' }}
+                          />
+                        </Button>
+
+                        <Popover
+                          triggerRef={triggerRef}
+                          isOpen={menuGoCardlessOpen}
+                          onOpenChange={() => setGoCardlessMenuOpen(false)}
+                        >
+                          <Menu
+                            onMenuSelect={item => {
+                              if (item === 'reconfigure') {
+                                onGoCardlessReset();
+                              }
+                            }}
+                            items={[
+                              {
+                                name: 'reconfigure',
+                                text: 'Reset GoCardless credentials',
+                              },
+                            ]}
+                          />
+                        </Popover>
+                      </>
+                    )}
+                  </View>
+                  <Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
+                    <strong>
+                      Link a <em>European</em> bank account
+                    </strong>{' '}
+                    to automatically download transactions. GoCardless provides
+                    reliable, up-to-date information from hundreds of banks.
+                  </Text>
+                  {simpleFinSyncFeatureFlag === true && (
+                    <>
+                      <View
                         style={{
-                          padding: '10px 0',
-                          fontSize: 15,
-                          fontWeight: 600,
-                          flex: 1,
+                          flexDirection: 'row',
+                          gap: 10,
+                          marginTop: '18px',
+                          alignItems: 'center',
                         }}
-                        onPress={onConnectSimpleFin}
                       >
-                        {isSimpleFinSetupComplete
-                          ? 'Link bank account with SimpleFIN'
-                          : 'Set up SimpleFIN for bank sync'}
-                      </ButtonWithLoading>
-                      {isSimpleFinSetupComplete && (
-                        <>
-                          <Button
-                            ref={triggerRef}
-                            variant="bare"
-                            onPress={() => setSimplefinMenuOpen(true)}
-                            aria-label="SimpleFIN menu"
-                          >
-                            <SvgDotsHorizontalTriple
-                              width={15}
-                              height={15}
-                              style={{ transform: 'rotateZ(90deg)' }}
-                            />
-                          </Button>
-                          <Popover
-                            triggerRef={triggerRef}
-                            isOpen={menuSimplefinOpen}
-                            onOpenChange={() => setSimplefinMenuOpen(false)}
-                          >
-                            <Menu
-                              onMenuSelect={item => {
-                                if (item === 'reconfigure') {
-                                  onSimpleFinReset();
-                                }
-                              }}
-                              items={[
-                                {
-                                  name: 'reconfigure',
-                                  text: 'Reset SimpleFIN credentials',
-                                },
-                              ]}
-                            />
-                          </Popover>
-                        </>
-                      )}
-                    </View>
-                    <Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
-                      <strong>
-                        Link a <em>North American</em> bank account
-                      </strong>{' '}
-                      to automatically download transactions. SimpleFIN provides
-                      reliable, up-to-date information from hundreds of banks.
-                    </Text>
-                  </>
-                )}
-              </>
-            ) : (
-              <>
-                <Button
-                  isDisabled
-                  style={{
-                    padding: '10px 0',
-                    fontSize: 15,
-                    fontWeight: 600,
-                  }}
-                >
-                  Set up bank sync
-                </Button>
-                <Paragraph style={{ fontSize: 15 }}>
-                  Connect to an Actual server to set up{' '}
-                  <Link
-                    variant="external"
-                    to="https://actualbudget.org/docs/advanced/bank-sync"
-                    linkColor="muted"
+                        <ButtonWithLoading
+                          isDisabled={syncServerStatus !== 'online'}
+                          isLoading={loadingSimpleFinAccounts}
+                          style={{
+                            padding: '10px 0',
+                            fontSize: 15,
+                            fontWeight: 600,
+                            flex: 1,
+                          }}
+                          onPress={onConnectSimpleFin}
+                        >
+                          {isSimpleFinSetupComplete
+                            ? 'Link bank account with SimpleFIN'
+                            : 'Set up SimpleFIN for bank sync'}
+                        </ButtonWithLoading>
+                        {isSimpleFinSetupComplete && (
+                          <>
+                            <Button
+                              ref={triggerRef}
+                              variant="bare"
+                              onPress={() => setSimplefinMenuOpen(true)}
+                              aria-label="SimpleFIN menu"
+                            >
+                              <SvgDotsHorizontalTriple
+                                width={15}
+                                height={15}
+                                style={{ transform: 'rotateZ(90deg)' }}
+                              />
+                            </Button>
+                            <Popover
+                              triggerRef={triggerRef}
+                              isOpen={menuSimplefinOpen}
+                              onOpenChange={() => setSimplefinMenuOpen(false)}
+                            >
+                              <Menu
+                                onMenuSelect={item => {
+                                  if (item === 'reconfigure') {
+                                    onSimpleFinReset();
+                                  }
+                                }}
+                                items={[
+                                  {
+                                    name: 'reconfigure',
+                                    text: 'Reset SimpleFIN credentials',
+                                  },
+                                ]}
+                              />
+                            </Popover>
+                          </>
+                        )}
+                      </View>
+                      <Text style={{ lineHeight: '1.4em', fontSize: 15 }}>
+                        <strong>
+                          Link a <em>North American</em> bank account
+                        </strong>{' '}
+                        to automatically download transactions. SimpleFIN
+                        provides reliable, up-to-date information from hundreds
+                        of banks.
+                      </Text>
+                    </>
+                  )}
+                </>
+              ) : (
+                <>
+                  <Button
+                    isDisabled
+                    style={{
+                      padding: '10px 0',
+                      fontSize: 15,
+                      fontWeight: 600,
+                    }}
                   >
-                    automatic syncing
-                  </Link>
-                  .
-                </Paragraph>
-              </>
-            )}
+                    Set up bank sync
+                  </Button>
+                  <Paragraph style={{ fontSize: 15 }}>
+                    Connect to an Actual server to set up{' '}
+                    <Link
+                      variant="external"
+                      to="https://actualbudget.org/docs/advanced/bank-sync"
+                      linkColor="muted"
+                    >
+                      automatic syncing
+                    </Link>
+                    .
+                  </Paragraph>
+                </>
+              )}
+            </View>
           </View>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
index 1c39b5aa9fe9ef7cf8773c15f788bc75d3017f36..8844739a4900f2cb53dd64c854988eac5ab53209 100644
--- a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
+++ b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx
@@ -1,5 +1,6 @@
 // @ts-strict-ignore
 import React, { useState } from 'react';
+import { Form } from 'react-aria-components';
 import { useDispatch } from 'react-redux';
 
 import { css } from 'glamor';
@@ -14,21 +15,23 @@ import { ButtonWithLoading } from '../common/Button2';
 import { InitialFocus } from '../common/InitialFocus';
 import { Input } from '../common/Input';
 import { Link } from '../common/Link';
-import { Modal, ModalButtons } from '../common/Modal';
+import {
+  Modal,
+  ModalButtons,
+  ModalCloseButton,
+  ModalHeader,
+} from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type CreateEncryptionKeyModalProps = {
-  modalProps: CommonModalProps;
   options: {
     recreate?: boolean;
   };
 };
 
 export function CreateEncryptionKeyModal({
-  modalProps,
   options = {},
 }: CreateEncryptionKeyModalProps) {
   const [password, setPassword] = useState('');
@@ -57,146 +60,153 @@ export function CreateEncryptionKeyModal({
       dispatch(sync());
 
       setLoading(false);
-      modalProps.onClose();
     }
   }
 
   return (
-    <Modal
-      {...modalProps}
-      title={isRecreating ? 'Generate new key' : 'Enable encryption'}
-      onClose={modalProps.onClose}
-    >
-      <View
-        style={{
-          maxWidth: 600,
-          overflowX: 'hidden',
-          overflowY: 'auto',
-          flex: 1,
-        }}
-      >
-        {!isRecreating ? (
-          <>
-            <Paragraph style={{ marginTop: 5 }}>
-              To enable end-to-end encryption, you need to create a key. We will
-              generate a key based on a password and use it to encrypt from now
-              on. <strong>This requires a sync reset</strong> and all other
-              devices will have to revert to this version of your data.{' '}
-              <Link
-                variant="external"
-                to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
-                linkColor="purple"
-              >
-                Learn more
-              </Link>
-            </Paragraph>
-            <Paragraph>
-              <ul
-                className={`${css({
-                  marginTop: 0,
-                  '& li': { marginBottom: 8 },
-                })}`}
-              >
-                <li>
-                  <strong>Important:</strong> if you forget this password{' '}
-                  <em>and</em> you don’t have any local copies of your data, you
-                  will lose access to all your data. The data cannot be
-                  decrypted without the password.
-                </li>
-                <li>
-                  This key only applies to this file. You will need to generate
-                  a new key for each file you want to encrypt.
-                </li>
-                <li>
-                  If you’ve already downloaded your data on other devices, you
-                  will need to reset them. Actual will automatically take you
-                  through this process.
-                </li>
-                <li>
-                  It is recommended for the encryption password to be different
-                  than the log-in password in order to better protect your data.
-                </li>
-              </ul>
-            </Paragraph>
-          </>
-        ) : (
-          <>
-            <Paragraph style={{ marginTop: 5 }}>
-              This will generate a new key for encrypting your data.{' '}
-              <strong>This requires a sync reset</strong> and all other devices
-              will have to revert to this version of your data. Actual will take
-              you through that process on those devices.{' '}
-              <Link
-                variant="external"
-                to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
-                linkColor="purple"
-              >
-                Learn more
-              </Link>
-            </Paragraph>
-            <Paragraph>
-              Key generation is randomized. The same password will create
-              different keys, so this will change your key regardless of the
-              password being different.
-            </Paragraph>
-          </>
-        )}
-      </View>
-      <form
-        onSubmit={e => {
-          e.preventDefault();
-          onCreateKey();
-        }}
-      >
-        <View style={{ alignItems: 'center' }}>
-          <Text style={{ fontWeight: 600, marginBottom: 3 }}>Password</Text>
-
-          {error && (
-            <View
-              style={{
-                color: theme.errorText,
-                textAlign: 'center',
-                fontSize: 13,
-                marginBottom: 3,
-              }}
-            >
-              {error}
-            </View>
-          )}
-
-          <InitialFocus>
-            <Input
-              type={showPassword ? 'text' : 'password'}
-              style={{
-                width: isNarrowWidth ? '100%' : '50%',
-                height: isNarrowWidth ? styles.mobileMinHeight : undefined,
-              }}
-              onChange={e => setPassword(e.target.value)}
-            />
-          </InitialFocus>
-          <Text style={{ marginTop: 5 }}>
-            <label style={{ userSelect: 'none' }}>
-              <input
-                type="checkbox"
-                onClick={() => setShowPassword(!showPassword)}
-              />{' '}
-              Show password
-            </label>
-          </Text>
-        </View>
-
-        <ModalButtons style={{ marginTop: 20 }}>
-          <ButtonWithLoading
-            variant="primary"
+    <Modal name="create-encryption-key">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={isRecreating ? 'Generate new key' : 'Enable encryption'}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
             style={{
-              height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+              maxWidth: 600,
+              overflowX: 'hidden',
+              overflowY: 'auto',
+              flex: 1,
             }}
-            isLoading={loading}
           >
-            Enable
-          </ButtonWithLoading>
-        </ModalButtons>
-      </form>
+            {!isRecreating ? (
+              <>
+                <Paragraph style={{ marginTop: 5 }}>
+                  To enable end-to-end encryption, you need to create a key. We
+                  will generate a key based on a password and use it to encrypt
+                  from now on. <strong>This requires a sync reset</strong> and
+                  all other devices will have to revert to this version of your
+                  data.{' '}
+                  <Link
+                    variant="external"
+                    to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
+                    linkColor="purple"
+                  >
+                    Learn more
+                  </Link>
+                </Paragraph>
+                <Paragraph>
+                  <ul
+                    className={`${css({
+                      marginTop: 0,
+                      '& li': { marginBottom: 8 },
+                    })}`}
+                  >
+                    <li>
+                      <strong>Important:</strong> if you forget this password{' '}
+                      <em>and</em> you don’t have any local copies of your data,
+                      you will lose access to all your data. The data cannot be
+                      decrypted without the password.
+                    </li>
+                    <li>
+                      This key only applies to this file. You will need to
+                      generate a new key for each file you want to encrypt.
+                    </li>
+                    <li>
+                      If you’ve already downloaded your data on other devices,
+                      you will need to reset them. Actual will automatically
+                      take you through this process.
+                    </li>
+                    <li>
+                      It is recommended for the encryption password to be
+                      different than the log-in password in order to better
+                      protect your data.
+                    </li>
+                  </ul>
+                </Paragraph>
+              </>
+            ) : (
+              <>
+                <Paragraph style={{ marginTop: 5 }}>
+                  This will generate a new key for encrypting your data.{' '}
+                  <strong>This requires a sync reset</strong> and all other
+                  devices will have to revert to this version of your data.
+                  Actual will take you through that process on those devices.{' '}
+                  <Link
+                    variant="external"
+                    to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
+                    linkColor="purple"
+                  >
+                    Learn more
+                  </Link>
+                </Paragraph>
+                <Paragraph>
+                  Key generation is randomized. The same password will create
+                  different keys, so this will change your key regardless of the
+                  password being different.
+                </Paragraph>
+              </>
+            )}
+          </View>
+          <Form
+            onSubmit={e => {
+              e.preventDefault();
+              onCreateKey();
+              close();
+            }}
+          >
+            <View style={{ alignItems: 'center' }}>
+              <Text style={{ fontWeight: 600, marginBottom: 3 }}>Password</Text>
+
+              {error && (
+                <View
+                  style={{
+                    color: theme.errorText,
+                    textAlign: 'center',
+                    fontSize: 13,
+                    marginBottom: 3,
+                  }}
+                >
+                  {error}
+                </View>
+              )}
+
+              <InitialFocus>
+                <Input
+                  type={showPassword ? 'text' : 'password'}
+                  style={{
+                    width: isNarrowWidth ? '100%' : '50%',
+                    height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  }}
+                  onChange={e => setPassword(e.target.value)}
+                />
+              </InitialFocus>
+              <Text style={{ marginTop: 5 }}>
+                <label style={{ userSelect: 'none' }}>
+                  <input
+                    type="checkbox"
+                    onClick={() => setShowPassword(!showPassword)}
+                  />{' '}
+                  Show password
+                </label>
+              </Text>
+            </View>
+
+            <ModalButtons style={{ marginTop: 20 }}>
+              <ButtonWithLoading
+                type="submit"
+                style={{
+                  height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                }}
+                isLoading={loading}
+                variant="primary"
+              >
+                Enable
+              </ButtonWithLoading>
+            </ModalButtons>
+          </Form>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx
index 0e3dcc07917a837c7544b8437237b9630a7b386a..2689ddd1267215245fb92f1faef220e3e24454aa 100644
--- a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx
+++ b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx
@@ -1,10 +1,11 @@
 // @ts-strict-ignore
 import React, { type FormEvent, useState } from 'react';
 import { Form } from 'react-aria-components';
+import { useDispatch } from 'react-redux';
 
+import { closeModal, createAccount } from 'loot-core/client/actions';
 import { toRelaxedNumber } from 'loot-core/src/shared/util';
 
-import { type BoundActions } from '../../hooks/useActions';
 import { useNavigate } from '../../hooks/useNavigate';
 import { theme } from '../../style';
 import { Button } from '../common/Button2';
@@ -13,22 +14,20 @@ import { InitialFocus } from '../common/InitialFocus';
 import { InlineField } from '../common/InlineField';
 import { Input } from '../common/Input';
 import { Link } from '../common/Link';
-import { Modal, ModalButtons, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalButtons,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { Checkbox } from '../forms';
-import { type CommonModalProps } from '../Modals';
 
-type CreateLocalAccountProps = {
-  modalProps: CommonModalProps;
-  actions: BoundActions;
-};
-
-export function CreateLocalAccountModal({
-  modalProps,
-  actions,
-}: CreateLocalAccountProps) {
+export function CreateLocalAccountModal() {
   const navigate = useNavigate();
+  const dispatch = useDispatch();
   const [name, setName] = useState('');
   const [offbudget, setOffbudget] = useState(false);
   const [balance, setBalance] = useState('0');
@@ -48,132 +47,135 @@ export function CreateLocalAccountModal({
     setBalanceError(balanceError);
 
     if (!nameError && !balanceError) {
-      actions.closeModal();
-      const id = await actions.createAccount(
-        name,
-        toRelaxedNumber(balance),
-        offbudget,
+      dispatch(closeModal());
+      const id = await dispatch(
+        createAccount(name, toRelaxedNumber(balance), offbudget),
       );
       navigate('/accounts/' + id);
     }
   };
   return (
-    <Modal
-      title={<ModalTitle title="Create Local Account" shrinkOnOverflow />}
-      {...modalProps}
-    >
-      {() => (
-        <View>
-          <Form onSubmit={onSubmit}>
-            <InlineField label="Name" width="100%">
-              <InitialFocus>
-                <Input
-                  name="name"
-                  value={name}
-                  onChange={event => setName(event.target.value)}
-                  onBlur={event => {
-                    const name = event.target.value.trim();
-                    setName(name);
-                    if (name && nameError) {
-                      setNameError(false);
-                    }
-                  }}
-                  style={{ flex: 1 }}
-                />
-              </InitialFocus>
-            </InlineField>
-            {nameError && (
-              <FormError style={{ marginLeft: 75 }}>Name is required</FormError>
-            )}
-
-            <View
-              style={{
-                width: '100%',
-                flexDirection: 'row',
-                justifyContent: 'flex-end',
-              }}
-            >
-              <View style={{ flexDirection: 'column' }}>
-                <View
-                  style={{
-                    flexDirection: 'row',
-                    justifyContent: 'flex-end',
-                  }}
-                >
-                  <Checkbox
-                    id="offbudget"
-                    name="offbudget"
-                    checked={offbudget}
-                    onChange={() => setOffbudget(!offbudget)}
+    <Modal name="add-local-account">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title="Create Local Account" shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <Form onSubmit={onSubmit}>
+              <InlineField label="Name" width="100%">
+                <InitialFocus>
+                  <Input
+                    name="name"
+                    value={name}
+                    onChange={event => setName(event.target.value)}
+                    onBlur={event => {
+                      const name = event.target.value.trim();
+                      setName(name);
+                      if (name && nameError) {
+                        setNameError(false);
+                      }
+                    }}
+                    style={{ flex: 1 }}
                   />
-                  <label
-                    htmlFor="offbudget"
+                </InitialFocus>
+              </InlineField>
+              {nameError && (
+                <FormError style={{ marginLeft: 75 }}>
+                  Name is required
+                </FormError>
+              )}
+
+              <View
+                style={{
+                  width: '100%',
+                  flexDirection: 'row',
+                  justifyContent: 'flex-end',
+                }}
+              >
+                <View style={{ flexDirection: 'column' }}>
+                  <View
                     style={{
-                      userSelect: 'none',
-                      verticalAlign: 'center',
+                      flexDirection: 'row',
+                      justifyContent: 'flex-end',
                     }}
                   >
-                    Off-budget
-                  </label>
-                </View>
-                <div
-                  style={{
-                    textAlign: 'right',
-                    fontSize: '0.7em',
-                    color: theme.pageTextLight,
-                    marginTop: 3,
-                  }}
-                >
-                  <Text>
-                    This cannot be changed later. <br /> {'\n'}
-                    See{' '}
-                    <Link
-                      variant="external"
-                      linkColor="muted"
-                      to="https://actualbudget.org/docs/accounts/#off-budget-accounts"
+                    <Checkbox
+                      id="offbudget"
+                      name="offbudget"
+                      checked={offbudget}
+                      onChange={() => setOffbudget(!offbudget)}
+                    />
+                    <label
+                      htmlFor="offbudget"
+                      style={{
+                        userSelect: 'none',
+                        verticalAlign: 'center',
+                      }}
                     >
-                      Accounts Overview
-                    </Link>{' '}
-                    for more information.
-                  </Text>
-                </div>
+                      Off-budget
+                    </label>
+                  </View>
+                  <div
+                    style={{
+                      textAlign: 'right',
+                      fontSize: '0.7em',
+                      color: theme.pageTextLight,
+                      marginTop: 3,
+                    }}
+                  >
+                    <Text>
+                      This cannot be changed later. <br /> {'\n'}
+                      See{' '}
+                      <Link
+                        variant="external"
+                        linkColor="muted"
+                        to="https://actualbudget.org/docs/accounts/#off-budget-accounts"
+                      >
+                        Accounts Overview
+                      </Link>{' '}
+                      for more information.
+                    </Text>
+                  </div>
+                </View>
               </View>
-            </View>
 
-            <InlineField label="Balance" width="100%">
-              <Input
-                name="balance"
-                inputMode="decimal"
-                value={balance}
-                onChange={event => setBalance(event.target.value)}
-                onBlur={event => {
-                  const balance = event.target.value.trim();
-                  setBalance(balance);
-                  if (validateBalance(balance) && balanceError) {
-                    setBalanceError(false);
-                  }
-                }}
-                style={{ flex: 1 }}
-              />
-            </InlineField>
-            {balanceError && (
-              <FormError style={{ marginLeft: 75 }}>
-                Balance must be a number
-              </FormError>
-            )}
+              <InlineField label="Balance" width="100%">
+                <Input
+                  name="balance"
+                  inputMode="decimal"
+                  value={balance}
+                  onChange={event => setBalance(event.target.value)}
+                  onBlur={event => {
+                    const balance = event.target.value.trim();
+                    setBalance(balance);
+                    if (validateBalance(balance) && balanceError) {
+                      setBalanceError(false);
+                    }
+                  }}
+                  style={{ flex: 1 }}
+                />
+              </InlineField>
+              {balanceError && (
+                <FormError style={{ marginLeft: 75 }}>
+                  Balance must be a number
+                </FormError>
+              )}
 
-            <ModalButtons>
-              <Button onPress={() => modalProps.onBack()}>Back</Button>
-              <Button
-                type="submit"
-                variant="primary"
-                style={{ marginLeft: 10 }}
-              >
-                Create
-              </Button>
-            </ModalButtons>
-          </Form>
-        </View>
+              <ModalButtons>
+                <Button onPress={close}>Back</Button>
+                <Button
+                  type="submit"
+                  variant="primary"
+                  style={{ marginLeft: 10 }}
+                >
+                  Create
+                </Button>
+              </ModalButtons>
+            </Form>
+          </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx
index 3ffc5168b2d7aaa85951adad6b5b940cf08fc64c..e34eb7e58ad657c507594b3d3e94ffcc63745f27 100644
--- a/packages/desktop-client/src/components/modals/EditField.jsx
+++ b/packages/desktop-client/src/components/modals/EditField.jsx
@@ -10,23 +10,18 @@ import { useResponsive } from '../../ResponsiveProvider';
 import { theme } from '../../style';
 import { Button } from '../common/Button2';
 import { Input } from '../common/Input';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
 import { SectionLabel } from '../forms';
 import { DateSelect } from '../select/DateSelect';
 
-export function EditField({ modalProps, name, onSubmit, onClose }) {
+export function EditField({ name, onSubmit, onClose }) {
   const dateFormat = useDateFormat() || 'MM/dd/yyyy';
-  const onCloseInner = () => {
-    modalProps.onClose();
-    onClose?.();
-  };
 
   function onSelectNote(value, mode) {
     if (value != null) {
       onSubmit(name, value, mode);
     }
-    onCloseInner();
   }
 
   function onSelect(value) {
@@ -38,7 +33,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
 
       onSubmit(name, value);
     }
-    onCloseInner();
   }
 
   const itemStyle = {
@@ -62,7 +56,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
       const today = currentDay();
       label = 'Date';
       minWidth = 350;
-      editor = (
+      editor = ({ close }) => (
         <DateSelect
           value={formatDate(parseISO(today), dateFormat)}
           dateFormat={dateFormat}
@@ -71,6 +65,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
           onUpdate={() => {}}
           onSelect={date => {
             onSelect(dayFromDate(parseDate(date, 'yyyy-MM-dd', new Date())));
+            close();
           }}
         />
       );
@@ -78,7 +73,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
 
     case 'notes':
       label = 'Notes';
-      editor = (
+      editor = ({ close }) => (
         <>
           <View
             style={{
@@ -192,7 +187,10 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
             id="noteInput"
             autoFocus
             focused={true}
-            onEnter={e => onSelectNote(e.target.value, noteAmend)}
+            onEnter={e => {
+              onSelectNote(e.target.value, noteAmend);
+              close();
+            }}
             style={inputStyle}
           />
         </>
@@ -201,10 +199,13 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
 
     case 'amount':
       label = 'Amount';
-      editor = (
+      editor = ({ close }) => (
         <Input
           focused={true}
-          onEnter={e => onSelect(e.target.value)}
+          onEnter={e => {
+            onSelect(e.target.value);
+            close();
+          }}
           style={inputStyle}
         />
       );
@@ -215,34 +216,40 @@ export function EditField({ modalProps, name, onSubmit, onClose }) {
 
   return (
     <Modal
-      title={label}
+      name="edit-field"
       noAnimation={!isNarrowWidth}
-      showHeader={isNarrowWidth}
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={onCloseInner}
-      style={{
-        flex: 0,
-        height: isNarrowWidth ? '85vh' : 275,
-        padding: '15px 10px',
-        ...(minWidth && { minWidth }),
-        backgroundColor: theme.menuAutoCompleteBackground,
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: isNarrowWidth ? '85vh' : 275,
+          padding: '15px 10px',
+          ...(minWidth && { minWidth }),
+          backgroundColor: theme.menuAutoCompleteBackground,
+        },
       }}
     >
-      {() => (
-        <View>
-          {!isNarrowWidth && (
-            <SectionLabel
+      {({ state: { close } }) => (
+        <>
+          {isNarrowWidth && (
+            <ModalHeader
               title={label}
-              style={{
-                alignSelf: 'center',
-                color: theme.menuAutoCompleteText,
-                marginBottom: 10,
-              }}
+              rightContent={<ModalCloseButton onClick={close} />}
             />
           )}
-          <View style={{ flex: 1 }}>{editor}</View>
-        </View>
+          <View>
+            {!isNarrowWidth && (
+              <SectionLabel
+                title={label}
+                style={{
+                  alignSelf: 'center',
+                  color: theme.menuAutoCompleteText,
+                  marginBottom: 10,
+                }}
+              />
+            )}
+            <View style={{ flex: 1 }}>{editor({ close })}</View>
+          </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx
index b54ec5d654fe92815e7a865a0346576e4ff90dc9..2256bc2877563674cca9dab9343c7078ab1b532a 100644
--- a/packages/desktop-client/src/components/modals/EditRule.jsx
+++ b/packages/desktop-client/src/components/modals/EditRule.jsx
@@ -42,7 +42,7 @@ import { SvgInformationOutline } from '../../icons/v1';
 import { styles, theme } from '../../style';
 import { Button } from '../common/Button2';
 import { Menu } from '../common/Menu';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Select } from '../common/Select';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
@@ -671,7 +671,7 @@ const conditionFields = [
     ['amount-outflow', mapField('amount', { outflow: true })],
   ]);
 
-export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
+export function EditRule({ defaultRule, onSave: originalOnSave }) {
   const [conditions, setConditions] = useState(
     defaultRule.conditions.map(parse),
   );
@@ -888,7 +888,6 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
       }
 
       originalOnSave?.(rule);
-      modalProps.onClose();
     }
   }
 
@@ -902,256 +901,264 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) {
   const showSplitButton = actionSplits.length > 0;
 
   return (
-    <Modal
-      title="Rule"
-      {...modalProps}
-      style={{ ...modalProps.style, flex: 'inherit' }}
-    >
-      {() => (
-        <View
-          style={{
-            maxWidth: '100%',
-            width: 900,
-            height: '80vh',
-            flexGrow: 0,
-            flexShrink: 0,
-            flexBasis: 'auto',
-            overflow: 'hidden',
-            color: theme.pageTextLight,
-          }}
-        >
+    <Modal name="edit-rule">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Rule"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
           <View
             style={{
-              flexDirection: 'row',
-              alignItems: 'center',
-              marginBottom: 15,
-              padding: '0 20px',
+              maxWidth: '100%',
+              width: 900,
+              height: '80vh',
+              flexGrow: 0,
+              flexShrink: 0,
+              flexBasis: 'auto',
+              overflow: 'hidden',
+              color: theme.pageTextLight,
             }}
           >
-            <Text style={{ marginRight: 15 }}>Stage of rule:</Text>
-
-            <Stack direction="row" align="center" spacing={1}>
-              <StageButton
-                selected={stage === 'pre'}
-                onSelect={() => onChangeStage('pre')}
-              >
-                Pre
-              </StageButton>
-              <StageButton
-                selected={stage === null}
-                onSelect={() => onChangeStage(null)}
-              >
-                Default
-              </StageButton>
-              <StageButton
-                selected={stage === 'post'}
-                onSelect={() => onChangeStage('post')}
-              >
-                Post
-              </StageButton>
-
-              <StageInfo />
-            </Stack>
-          </View>
+            <View
+              style={{
+                flexDirection: 'row',
+                alignItems: 'center',
+                marginBottom: 15,
+                padding: '0 20px',
+              }}
+            >
+              <Text style={{ marginRight: 15 }}>Stage of rule:</Text>
 
-          <View
-            innerRef={scrollableEl}
-            style={{
-              borderBottom: '1px solid ' + theme.tableBorder,
-              padding: 20,
-              overflow: 'auto',
-              maxHeight: 'calc(100% - 300px)',
-            }}
-          >
-            <View style={{ flexShrink: 0 }}>
-              <View style={{ marginBottom: 30 }}>
-                <Text style={{ marginBottom: 15 }}>
-                  If
-                  <FieldSelect
-                    data-testid="conditions-op"
-                    style={{ display: 'inline-flex' }}
-                    fields={[
-                      ['and', 'all'],
-                      ['or', 'any'],
-                    ]}
-                    value={conditionsOp}
-                    onChange={onChangeConditionsOp}
-                  />
-                  of these conditions match:
-                </Text>
+              <Stack direction="row" align="center" spacing={1}>
+                <StageButton
+                  selected={stage === 'pre'}
+                  onSelect={() => onChangeStage('pre')}
+                >
+                  Pre
+                </StageButton>
+                <StageButton
+                  selected={stage === null}
+                  onSelect={() => onChangeStage(null)}
+                >
+                  Default
+                </StageButton>
+                <StageButton
+                  selected={stage === 'post'}
+                  onSelect={() => onChangeStage('post')}
+                >
+                  Post
+                </StageButton>
 
-                <ConditionsList
-                  conditionsOp={conditionsOp}
-                  conditions={conditions}
-                  editorStyle={editorStyle}
-                  isSchedule={isSchedule}
-                  onChangeConditions={conds => setConditions(conds)}
-                />
-              </View>
+                <StageInfo />
+              </Stack>
+            </View>
 
-              <Text style={{ marginBottom: 15 }}>
-                Then apply these actions:
-              </Text>
-              <View style={{ flex: 1 }}>
-                {actionSplits.length === 0 && (
-                  <Button
-                    style={{ alignSelf: 'flex-start' }}
-                    onPress={addInitialAction}
-                  >
-                    Add action
-                  </Button>
-                )}
-                <Stack spacing={2} data-testid="action-split-list">
-                  {actionSplits.map(({ id, actions }, splitIndex) => (
-                    <View
-                      key={id}
-                      nativeStyle={
-                        actionSplits.length > 1
-                          ? {
-                              borderColor: theme.tableBorder,
-                              borderWidth: '1px',
-                              borderRadius: '5px',
-                              padding: '5px',
-                            }
-                          : {}
-                      }
+            <View
+              innerRef={scrollableEl}
+              style={{
+                borderBottom: '1px solid ' + theme.tableBorder,
+                padding: 20,
+                overflow: 'auto',
+                maxHeight: 'calc(100% - 300px)',
+              }}
+            >
+              <View style={{ flexShrink: 0 }}>
+                <View style={{ marginBottom: 30 }}>
+                  <Text style={{ marginBottom: 15 }}>
+                    If
+                    <FieldSelect
+                      data-testid="conditions-op"
+                      style={{ display: 'inline-flex' }}
+                      fields={[
+                        ['and', 'all'],
+                        ['or', 'any'],
+                      ]}
+                      value={conditionsOp}
+                      onChange={onChangeConditionsOp}
+                    />
+                    of these conditions match:
+                  </Text>
+
+                  <ConditionsList
+                    conditionsOp={conditionsOp}
+                    conditions={conditions}
+                    editorStyle={editorStyle}
+                    isSchedule={isSchedule}
+                    onChangeConditions={conds => setConditions(conds)}
+                  />
+                </View>
+
+                <Text style={{ marginBottom: 15 }}>
+                  Then apply these actions:
+                </Text>
+                <View style={{ flex: 1 }}>
+                  {actionSplits.length === 0 && (
+                    <Button
+                      style={{ alignSelf: 'flex-start' }}
+                      onPress={addInitialAction}
                     >
-                      {actionSplits.length > 1 && (
-                        <Stack
-                          direction="row"
-                          justify="space-between"
-                          spacing={1}
-                        >
-                          <Text
-                            style={{
-                              ...styles.smallText,
-                              marginBottom: '10px',
-                            }}
+                      Add action
+                    </Button>
+                  )}
+                  <Stack spacing={2} data-testid="action-split-list">
+                    {actionSplits.map(({ id, actions }, splitIndex) => (
+                      <View
+                        key={id}
+                        nativeStyle={
+                          actionSplits.length > 1
+                            ? {
+                                borderColor: theme.tableBorder,
+                                borderWidth: '1px',
+                                borderRadius: '5px',
+                                padding: '5px',
+                              }
+                            : {}
+                        }
+                      >
+                        {actionSplits.length > 1 && (
+                          <Stack
+                            direction="row"
+                            justify="space-between"
+                            spacing={1}
                           >
-                            {splitIndex === 0
-                              ? 'Apply to all'
-                              : `Split ${splitIndex}`}
-                          </Text>
-                          {splitIndex && (
-                            <Button
-                              variant="bare"
-                              onPress={() => onRemoveSplit(splitIndex)}
+                            <Text
                               style={{
-                                width: 20,
-                                height: 20,
+                                ...styles.smallText,
+                                marginBottom: '10px',
                               }}
-                              aria-label="Delete split"
                             >
-                              <SvgDelete
+                              {splitIndex === 0
+                                ? 'Apply to all'
+                                : `Split ${splitIndex}`}
+                            </Text>
+                            {splitIndex && (
+                              <Button
+                                variant="bare"
+                                onPress={() => onRemoveSplit(splitIndex)}
                                 style={{
-                                  width: 8,
-                                  height: 8,
-                                  color: 'inherit',
+                                  width: 20,
+                                  height: 20,
                                 }}
+                                aria-label="Delete split"
+                              >
+                                <SvgDelete
+                                  style={{
+                                    width: 8,
+                                    height: 8,
+                                    color: 'inherit',
+                                  }}
+                                />
+                              </Button>
+                            )}
+                          </Stack>
+                        )}
+                        <Stack spacing={2} data-testid="action-list">
+                          {actions.map((action, actionIndex) => (
+                            <View key={actionIndex}>
+                              <ActionEditor
+                                ops={['set', 'link-schedule']}
+                                action={action}
+                                editorStyle={editorStyle}
+                                onChange={(name, value) => {
+                                  onChangeAction(action, name, value);
+                                }}
+                                onDelete={() => onRemoveAction(action)}
+                                onAdd={() =>
+                                  addActionToSplitAfterIndex(
+                                    splitIndex,
+                                    actionIndex,
+                                  )
+                                }
                               />
-                            </Button>
-                          )}
+                            </View>
+                          ))}
                         </Stack>
-                      )}
-                      <Stack spacing={2} data-testid="action-list">
-                        {actions.map((action, actionIndex) => (
-                          <View key={actionIndex}>
-                            <ActionEditor
-                              ops={['set', 'link-schedule']}
-                              action={action}
-                              editorStyle={editorStyle}
-                              onChange={(name, value) => {
-                                onChangeAction(action, name, value);
-                              }}
-                              onDelete={() => onRemoveAction(action)}
-                              onAdd={() =>
-                                addActionToSplitAfterIndex(
-                                  splitIndex,
-                                  actionIndex,
-                                )
-                              }
-                            />
-                          </View>
-                        ))}
-                      </Stack>
-
-                      {actions.length === 0 && (
-                        <Button
-                          style={{ alignSelf: 'flex-start', marginTop: 5 }}
-                          onPress={() =>
-                            addActionToSplitAfterIndex(splitIndex, -1)
-                          }
-                        >
-                          Add action
-                        </Button>
-                      )}
-                    </View>
-                  ))}
-                </Stack>
-                {showSplitButton && (
+
+                        {actions.length === 0 && (
+                          <Button
+                            style={{ alignSelf: 'flex-start', marginTop: 5 }}
+                            onPress={() =>
+                              addActionToSplitAfterIndex(splitIndex, -1)
+                            }
+                          >
+                            Add action
+                          </Button>
+                        )}
+                      </View>
+                    ))}
+                  </Stack>
+                  {showSplitButton && (
+                    <Button
+                      style={{ alignSelf: 'flex-start', marginTop: 15 }}
+                      onPress={() => {
+                        addActionToSplitAfterIndex(actionSplits.length, -1);
+                      }}
+                      data-testid="add-split-transactions"
+                    >
+                      {actionSplits.length > 1
+                        ? 'Add another split'
+                        : 'Split into multiple transactions'}
+                    </Button>
+                  )}
+                </View>
+              </View>
+            </View>
+
+            <SelectedProvider instance={selectedInst}>
+              <View style={{ padding: '20px', flex: 1 }}>
+                <View
+                  style={{
+                    flexDirection: 'row',
+                    alignItems: 'center',
+                    marginBottom: 12,
+                  }}
+                >
+                  <Text style={{ color: theme.pageTextLight, marginBottom: 0 }}>
+                    This rule applies to these transactions:
+                  </Text>
+
+                  <View style={{ flex: 1 }} />
+                  <Button
+                    isDisabled={selectedInst.items.size === 0}
+                    onPress={onApply}
+                  >
+                    Apply actions ({selectedInst.items.size})
+                  </Button>
+                </View>
+
+                <SimpleTransactionsTable
+                  transactions={transactions}
+                  fields={getTransactionFields(
+                    conditions,
+                    getActions(actionSplits),
+                  )}
+                  style={{
+                    border: '1px solid ' + theme.tableBorder,
+                    borderRadius: '6px 6px 0 0',
+                  }}
+                />
+
+                <Stack
+                  direction="row"
+                  justify="flex-end"
+                  style={{ marginTop: 20 }}
+                >
+                  <Button onClick={close}>Cancel</Button>
                   <Button
-                    style={{ alignSelf: 'flex-start', marginTop: 15 }}
+                    variant="primary"
                     onPress={() => {
-                      addActionToSplitAfterIndex(actionSplits.length, -1);
+                      onSave();
+                      close();
                     }}
-                    data-testid="add-split-transactions"
                   >
-                    {actionSplits.length > 1
-                      ? 'Add another split'
-                      : 'Split into multiple transactions'}
+                    Save
                   </Button>
-                )}
+                </Stack>
               </View>
-            </View>
+            </SelectedProvider>
           </View>
-
-          <SelectedProvider instance={selectedInst}>
-            <View style={{ padding: '20px', flex: 1 }}>
-              <View
-                style={{
-                  flexDirection: 'row',
-                  alignItems: 'center',
-                  marginBottom: 12,
-                }}
-              >
-                <Text style={{ color: theme.pageTextLight, marginBottom: 0 }}>
-                  This rule applies to these transactions:
-                </Text>
-
-                <View style={{ flex: 1 }} />
-                <Button
-                  isDisabled={selectedInst.items.size === 0}
-                  onPress={onApply}
-                >
-                  Apply actions ({selectedInst.items.size})
-                </Button>
-              </View>
-
-              <SimpleTransactionsTable
-                transactions={transactions}
-                fields={getTransactionFields(
-                  conditions,
-                  getActions(actionSplits),
-                )}
-                style={{
-                  border: '1px solid ' + theme.tableBorder,
-                  borderRadius: '6px 6px 0 0',
-                }}
-              />
-
-              <Stack
-                direction="row"
-                justify="flex-end"
-                style={{ marginTop: 20 }}
-              >
-                <Button onPress={() => modalProps.onClose()}>Cancel</Button>
-                <Button variant="primary" onPress={() => onSave()}>
-                  Save
-                </Button>
-              </Stack>
-            </View>
-          </SelectedProvider>
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx
index 4efbe26ed4de31bcab4ca9c3849f19b37fac684f..c48a6e175fe92452f98137c81fdb087ae8ef49be 100644
--- a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx
+++ b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx
@@ -12,19 +12,21 @@ import { Button, ButtonWithLoading } from '../common/Button2';
 import { InitialFocus } from '../common/InitialFocus';
 import { Input } from '../common/Input';
 import { Link } from '../common/Link';
-import { Modal, ModalButtons } from '../common/Modal';
+import {
+  Modal,
+  ModalButtons,
+  ModalCloseButton,
+  ModalHeader,
+} from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type FixEncryptionKeyModalProps = {
-  modalProps: CommonModalProps;
   options: FinanceModals['fix-encryption-key'];
 };
 
 export function FixEncryptionKeyModal({
-  modalProps,
   options = {},
 }: FixEncryptionKeyModalProps) {
   const { hasExistingKey, cloudFileId, onSuccess } = options;
@@ -50,122 +52,128 @@ export function FixEncryptionKeyModal({
         return;
       }
 
-      modalProps.onClose();
       onSuccess?.();
     }
   }
 
   return (
-    <Modal
-      {...modalProps}
-      title={
-        hasExistingKey ? 'Unable to decrypt file' : 'This file is encrypted'
-      }
-      onClose={modalProps.onClose}
-    >
-      <View
-        style={{
-          maxWidth: 500,
-          overflowX: 'hidden',
-          overflowY: 'auto',
-          flex: 1,
-        }}
-      >
-        {hasExistingKey ? (
-          <Paragraph>
-            This file was encrypted with a different key than you are currently
-            using. This probably means you changed your password. Enter your
-            current password to update your key.{' '}
-            <Link
-              variant="external"
-              to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
-            >
-              Learn more
-            </Link>
-          </Paragraph>
-        ) : (
-          <Paragraph>
-            We don’t have a key that encrypts or decrypts this file. Enter the
-            password for this file to create the key for encryption.{' '}
-            <Link
-              variant="external"
-              to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
-            >
-              Learn more
-            </Link>
-          </Paragraph>
-        )}
-      </View>
-      <Form
-        onSubmit={e => {
-          e.preventDefault();
-          onUpdateKey();
-        }}
-      >
-        <View
-          style={{
-            marginTop: 15,
-            flexDirection: 'column',
-            alignItems: 'center',
-          }}
-        >
-          <Text style={{ fontWeight: 600, marginBottom: 5 }}>Password</Text>{' '}
-          {error && (
+    <Modal name="fix-encryption-key">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={
+              hasExistingKey
+                ? 'Unable to decrypt file'
+                : 'This file is encrypted'
+            }
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              maxWidth: 500,
+              overflowX: 'hidden',
+              overflowY: 'auto',
+              flex: 1,
+            }}
+          >
+            {hasExistingKey ? (
+              <Paragraph>
+                This file was encrypted with a different key than you are
+                currently using. This probably means you changed your password.
+                Enter your current password to update your key.{' '}
+                <Link
+                  variant="external"
+                  to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
+                >
+                  Learn more
+                </Link>
+              </Paragraph>
+            ) : (
+              <Paragraph>
+                We don’t have a key that encrypts or decrypts this file. Enter
+                the password for this file to create the key for encryption.{' '}
+                <Link
+                  variant="external"
+                  to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption"
+                >
+                  Learn more
+                </Link>
+              </Paragraph>
+            )}
+          </View>
+          <Form
+            onSubmit={e => {
+              e.preventDefault();
+              onUpdateKey();
+              close();
+            }}
+          >
             <View
               style={{
-                color: theme.errorText,
-                textAlign: 'center',
-                fontSize: 13,
-                marginBottom: 3,
+                marginTop: 15,
+                flexDirection: 'column',
+                alignItems: 'center',
               }}
             >
-              {error}
+              <Text style={{ fontWeight: 600, marginBottom: 5 }}>Password</Text>{' '}
+              {error && (
+                <View
+                  style={{
+                    color: theme.errorText,
+                    textAlign: 'center',
+                    fontSize: 13,
+                    marginBottom: 3,
+                  }}
+                >
+                  {error}
+                </View>
+              )}
+              <InitialFocus>
+                <Input
+                  type={showPassword ? 'text' : 'password'}
+                  style={{
+                    width: isNarrowWidth ? '100%' : '50%',
+                    height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  }}
+                  onChange={e => setPassword(e.target.value)}
+                />
+              </InitialFocus>
+              <Text style={{ marginTop: 5 }}>
+                <label style={{ userSelect: 'none' }}>
+                  <input
+                    type="checkbox"
+                    onClick={() => setShowPassword(!showPassword)}
+                  />{' '}
+                  Show password
+                </label>
+              </Text>
             </View>
-          )}
-          <InitialFocus>
-            <Input
-              type={showPassword ? 'text' : 'password'}
-              style={{
-                width: isNarrowWidth ? '100%' : '50%',
-                height: isNarrowWidth ? styles.mobileMinHeight : undefined,
-              }}
-              onChange={e => setPassword(e.target.value)}
-            />
-          </InitialFocus>
-          <Text style={{ marginTop: 5 }}>
-            <label style={{ userSelect: 'none' }}>
-              <input
-                type="checkbox"
-                onClick={() => setShowPassword(!showPassword)}
-              />{' '}
-              Show password
-            </label>
-          </Text>
-        </View>
 
-        <ModalButtons style={{ marginTop: 20 }}>
-          <Button
-            variant="normal"
-            style={{
-              height: isNarrowWidth ? styles.mobileMinHeight : undefined,
-              marginRight: 10,
-            }}
-            onPress={() => modalProps.onBack()}
-          >
-            Back
-          </Button>
-          <ButtonWithLoading
-            type="submit"
-            variant="primary"
-            style={{
-              height: isNarrowWidth ? styles.mobileMinHeight : undefined,
-            }}
-            isLoading={loading}
-          >
-            {hasExistingKey ? 'Update key' : 'Create key'}
-          </ButtonWithLoading>
-        </ModalButtons>
-      </Form>
+            <ModalButtons style={{ marginTop: 20 }}>
+              <Button
+                variant="normal"
+                style={{
+                  height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                  marginRight: 10,
+                }}
+                onPress={close}
+              >
+                Back
+              </Button>
+              <ButtonWithLoading
+                type="submit"
+                variant="primary"
+                style={{
+                  height: isNarrowWidth ? styles.mobileMinHeight : undefined,
+                }}
+                isLoading={loading}
+              >
+                {hasExistingKey ? 'Update key' : 'Create key'}
+              </ButtonWithLoading>
+            </ModalButtons>
+          </Form>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx
index 9cb6e106bc3053649539082af458f9ee50744bad..2dbbc049eefbb2841f4c3572ac0fb743ee372b1b 100644
--- a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx
+++ b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx
@@ -16,11 +16,10 @@ import { Error, Warning } from '../alerts';
 import { Autocomplete } from '../autocomplete/Autocomplete';
 import { Button } from '../common/Button2';
 import { Link } from '../common/Link';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { View } from '../common/View';
 import { FormField, FormLabel } from '../forms';
-import { type CommonModalProps } from '../Modals';
 
 import { COUNTRY_OPTIONS } from './countries';
 
@@ -74,7 +73,6 @@ function renderError(error: 'unknown' | 'timeout') {
 }
 
 type GoCardlessExternalMsgProps = {
-  modalProps: CommonModalProps;
   onMoveExternal: (arg: {
     institutionId: string;
   }) => Promise<{ error?: 'unknown' | 'timeout'; data?: GoCardlessToken }>;
@@ -83,10 +81,9 @@ type GoCardlessExternalMsgProps = {
 };
 
 export function GoCardlessExternalMsg({
-  modalProps,
   onMoveExternal,
   onSuccess,
-  onClose: originalOnClose,
+  onClose,
 }: GoCardlessExternalMsgProps) {
   const dispatch = useDispatch();
 
@@ -126,11 +123,6 @@ export function GoCardlessExternalMsg({
     setSuccess(true);
   }
 
-  function onClose() {
-    originalOnClose?.();
-    modalProps.onClose();
-  }
-
   async function onContinue() {
     setWaiting('accounts');
     await onSuccess(data.current);
@@ -232,69 +224,78 @@ export function GoCardlessExternalMsg({
 
   return (
     <Modal
-      title="Link Your Bank"
-      {...modalProps}
+      name="gocardless-external-msg"
       onClose={onClose}
-      style={{ flex: 0 }}
+      containerProps={{ style: { width: '30vw' } }}
     >
-      {() => (
-        <View>
-          <Paragraph style={{ fontSize: 15 }}>
-            To link your bank account, you will be redirected to a new page
-            where GoCardless will ask to connect to your bank. GoCardless will
-            not be able to withdraw funds from your accounts.
-          </Paragraph>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Link Your Bank"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <Paragraph style={{ fontSize: 15 }}>
+              To link your bank account, you will be redirected to a new page
+              where GoCardless will ask to connect to your bank. GoCardless will
+              not be able to withdraw funds from your accounts.
+            </Paragraph>
 
-          {error && renderError(error)}
+            {error && renderError(error)}
 
-          {waiting || isConfigurationLoading ? (
-            <View style={{ alignItems: 'center', marginTop: 15 }}>
-              <AnimatedLoading
-                color={theme.pageTextDark}
-                style={{ width: 20, height: 20 }}
-              />
-              <View style={{ marginTop: 10, color: theme.pageText }}>
-                {isConfigurationLoading
-                  ? 'Checking GoCardless configuration..'
-                  : waiting === 'browser'
-                    ? 'Waiting on GoCardless...'
-                    : waiting === 'accounts'
-                      ? 'Loading accounts...'
-                      : null}
-              </View>
+            {waiting || isConfigurationLoading ? (
+              <View style={{ alignItems: 'center', marginTop: 15 }}>
+                <AnimatedLoading
+                  color={theme.pageTextDark}
+                  style={{ width: 20, height: 20 }}
+                />
+                <View style={{ marginTop: 10, color: theme.pageText }}>
+                  {isConfigurationLoading
+                    ? 'Checking GoCardless configuration..'
+                    : waiting === 'browser'
+                      ? 'Waiting on GoCardless...'
+                      : waiting === 'accounts'
+                        ? 'Loading accounts...'
+                        : null}
+                </View>
 
-              {waiting === 'browser' && (
-                <Link variant="text" onClick={onJump} style={{ marginTop: 10 }}>
-                  (Account linking not opening in a new tab? Click here)
-                </Link>
-              )}
-            </View>
-          ) : success ? (
-            <Button
-              variant="primary"
-              style={{
-                padding: '10px 0',
-                fontSize: 15,
-                fontWeight: 600,
-                marginTop: 10,
-              }}
-              onPress={onContinue}
-            >
-              Success! Click to continue &rarr;
-            </Button>
-          ) : isConfigured || isGoCardlessSetupComplete ? (
-            renderLinkButton()
-          ) : (
-            <>
-              <Paragraph style={{ color: theme.errorText }}>
-                GoCardless integration has not yet been configured.
-              </Paragraph>
-              <Button variant="primary" onPress={onGoCardlessInit}>
-                Configure GoCardless integration
+                {waiting === 'browser' && (
+                  <Link
+                    variant="text"
+                    onClick={onJump}
+                    style={{ marginTop: 10 }}
+                  >
+                    (Account linking not opening in a new tab? Click here)
+                  </Link>
+                )}
+              </View>
+            ) : success ? (
+              <Button
+                variant="primary"
+                style={{
+                  padding: '10px 0',
+                  fontSize: 15,
+                  fontWeight: 600,
+                  marginTop: 10,
+                }}
+                onPress={onContinue}
+              >
+                Success! Click to continue &rarr;
               </Button>
-            </>
-          )}
-        </View>
+            ) : isConfigured || isGoCardlessSetupComplete ? (
+              renderLinkButton()
+            ) : (
+              <>
+                <Paragraph style={{ color: theme.errorText }}>
+                  GoCardless integration has not yet been configured.
+                </Paragraph>
+                <Button variant="primary" onPress={onGoCardlessInit}>
+                  Configure GoCardless integration
+                </Button>
+              </>
+            )}
+          </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx b/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx
index 021d488a345df89d29bbcf4168b5524de1927823..3ba1ea3d0f9753f8eb359234b30bcbf71ea273f3 100644
--- a/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx
+++ b/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx
@@ -7,19 +7,21 @@ import { Error } from '../alerts';
 import { ButtonWithLoading } from '../common/Button2';
 import { Input } from '../common/Input';
 import { Link } from '../common/Link';
-import { Modal, ModalButtons } from '../common/Modal';
-import type { ModalProps } from '../common/Modal';
+import {
+  Modal,
+  ModalButtons,
+  ModalCloseButton,
+  ModalHeader,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { FormField, FormLabel } from '../forms';
 
 type GoCardlessInitialiseProps = {
-  modalProps?: Partial<ModalProps>;
   onSuccess: () => void;
 };
 
 export const GoCardlessInitialise = ({
-  modalProps,
   onSuccess,
 }: GoCardlessInitialiseProps) => {
   const [secretId, setSecretId] = useState('');
@@ -47,69 +49,79 @@ export const GoCardlessInitialise = ({
     ]);
 
     onSuccess();
-    modalProps.onClose();
     setIsLoading(false);
   };
 
   return (
-    <Modal title="Set-up GoCardless" size={{ width: 300 }} {...modalProps}>
-      <View style={{ display: 'flex', gap: 10 }}>
-        <Text>
-          In order to enable bank-sync via GoCardless (only for EU banks) you
-          will need to create access credentials. This can be done by creating
-          an account with{' '}
-          <Link
-            variant="external"
-            to="https://actualbudget.org/docs/advanced/bank-sync/"
-            linkColor="purple"
-          >
-            GoCardless
-          </Link>
-          .
-        </Text>
-
-        <FormField>
-          <FormLabel title="Secret ID:" htmlFor="secret-id-field" />
-          <Input
-            id="secret-id-field"
-            type="password"
-            value={secretId}
-            onChangeValue={value => {
-              setSecretId(value);
-              setIsValid(true);
-            }}
+    <Modal name="gocardless-init" containerProps={{ style: { width: '30vw' } }}>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Set-up GoCardless"
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </FormField>
+          <View style={{ display: 'flex', gap: 10 }}>
+            <Text>
+              In order to enable bank-sync via GoCardless (only for EU banks)
+              you will need to create access credentials. This can be done by
+              creating an account with{' '}
+              <Link
+                variant="external"
+                to="https://actualbudget.org/docs/advanced/bank-sync/"
+                linkColor="purple"
+              >
+                GoCardless
+              </Link>
+              .
+            </Text>
 
-        <FormField>
-          <FormLabel title="Secret Key:" htmlFor="secret-key-field" />
-          <Input
-            id="secret-key-field"
-            type="password"
-            value={secretKey}
-            onChangeValue={value => {
-              setSecretKey(value);
-              setIsValid(true);
-            }}
-          />
-        </FormField>
+            <FormField>
+              <FormLabel title="Secret ID:" htmlFor="secret-id-field" />
+              <Input
+                id="secret-id-field"
+                type="password"
+                value={secretId}
+                onChangeValue={value => {
+                  setSecretId(value);
+                  setIsValid(true);
+                }}
+              />
+            </FormField>
+
+            <FormField>
+              <FormLabel title="Secret Key:" htmlFor="secret-key-field" />
+              <Input
+                id="secret-key-field"
+                type="password"
+                value={secretKey}
+                onChangeValue={value => {
+                  setSecretKey(value);
+                  setIsValid(true);
+                }}
+              />
+            </FormField>
 
-        {!isValid && (
-          <Error>
-            It is required to provide both the secret id and secret key.
-          </Error>
-        )}
-      </View>
+            {!isValid && (
+              <Error>
+                It is required to provide both the secret id and secret key.
+              </Error>
+            )}
+          </View>
 
-      <ModalButtons>
-        <ButtonWithLoading
-          variant="primary"
-          isLoading={isLoading}
-          onPress={onSubmit}
-        >
-          Save and continue
-        </ButtonWithLoading>
-      </ModalButtons>
+          <ModalButtons>
+            <ButtonWithLoading
+              variant="primary"
+              isLoading={isLoading}
+              onPress={() => {
+                onSubmit();
+                close();
+              }}
+            >
+              Save and continue
+            </ButtonWithLoading>
+          </ModalButtons>
+        </>
+      )}
     </Modal>
   );
 };
diff --git a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx
index 292f92c8aaadcaa1eea71fdb8f6c2231db37b105..d2fe1cb4ac14715ef0aec5415285fce7126fce73 100644
--- a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx
+++ b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx
@@ -5,23 +5,18 @@ import { rolloverBudget } from 'loot-core/client/queries';
 import { styles } from '../../style';
 import { Button } from '../common/Button2';
 import { InitialFocus } from '../common/InitialFocus';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
 import { FieldLabel } from '../mobile/MobileForms';
-import { type CommonModalProps } from '../Modals';
 import { useSheetValue } from '../spreadsheet/useSheetValue';
 import { AmountInput } from '../util/AmountInput';
 
 type HoldBufferModalProps = {
-  modalProps: CommonModalProps;
   month: string;
   onSubmit: (amount: number) => void;
 };
 
-export function HoldBufferModal({
-  modalProps,
-  onSubmit,
-}: HoldBufferModalProps) {
+export function HoldBufferModal({ onSubmit }: HoldBufferModalProps) {
   const available = useSheetValue(rolloverBudget.toBudget);
   const [amount, setAmount] = useState<number>(0);
 
@@ -29,54 +24,58 @@ export function HoldBufferModal({
     if (newAmount) {
       onSubmit?.(newAmount);
     }
-
-    modalProps.onClose();
   };
 
   return (
-    <Modal
-      title="Hold Buffer"
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View>
-        <FieldLabel title="Hold this amount:" />
-        <InitialFocus>
-          <AmountInput
-            value={available}
-            autoDecimals={true}
+    <Modal name="hold-buffer">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Hold Buffer"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <FieldLabel title="Hold this amount:" />
+            <InitialFocus>
+              <AmountInput
+                value={available}
+                autoDecimals={true}
+                style={{
+                  marginLeft: styles.mobileEditingPadding,
+                  marginRight: styles.mobileEditingPadding,
+                }}
+                inputStyle={{
+                  height: styles.mobileMinHeight,
+                }}
+                onUpdate={setAmount}
+                onEnter={() => {
+                  _onSubmit(amount);
+                  close();
+                }}
+              />
+            </InitialFocus>
+          </View>
+          <View
             style={{
-              marginLeft: styles.mobileEditingPadding,
-              marginRight: styles.mobileEditingPadding,
+              justifyContent: 'center',
+              alignItems: 'center',
+              paddingTop: 10,
             }}
-            inputStyle={{
-              height: styles.mobileMinHeight,
-            }}
-            onUpdate={setAmount}
-            onEnter={() => _onSubmit(amount)}
-          />
-        </InitialFocus>
-      </View>
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          paddingTop: 10,
-        }}
-      >
-        <Button
-          variant="primary"
-          style={{
-            height: styles.mobileMinHeight,
-            marginLeft: styles.mobileEditingPadding,
-            marginRight: styles.mobileEditingPadding,
-          }}
-          onPress={() => _onSubmit(amount)}
-        >
-          Hold
-        </Button>
-      </View>
+          >
+            <Button
+              variant="primary"
+              style={{
+                height: styles.mobileMinHeight,
+                marginLeft: styles.mobileEditingPadding,
+                marginRight: styles.mobileEditingPadding,
+              }}
+              onPress={() => _onSubmit(amount)}
+            >
+              Hold
+            </Button>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
index 1c41776d74197afe788691376d001bd4e61b2ff5..56242cd4f56472818a72790a4af01145d819b35e 100644
--- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx
+++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx
@@ -14,9 +14,9 @@ import { useDateFormat } from '../../hooks/useDateFormat';
 import { useLocalPrefs } from '../../hooks/useLocalPrefs';
 import { SvgDownAndRightArrow } from '../../icons/v2';
 import { theme, styles } from '../../style';
-import { Button, ButtonWithLoading } from '../common/Button';
+import { Button, ButtonWithLoading } from '../common/Button2';
 import { Input } from '../common/Input';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Select } from '../common/Select';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
@@ -836,7 +836,7 @@ function FieldMappings({
   );
 }
 
-export function ImportTransactions({ modalProps, options }) {
+export function ImportTransactions({ options }) {
   const dateFormat = useDateFormat() || 'MM/dd/yyyy';
   const prefs = useLocalPrefs();
   const {
@@ -1206,8 +1206,6 @@ export function ImportTransactions({ modalProps, options }) {
     if (onImported) {
       onImported(didChange);
     }
-
-    modalProps.onClose();
   }
 
   const runImportPreviewCallback = useCallback(async () => {
@@ -1382,309 +1380,323 @@ export function ImportTransactions({ modalProps, options }) {
 
   return (
     <Modal
-      title={
-        'Import transactions' + (filetype ? ` (${filetype.toUpperCase()})` : '')
-      }
-      {...modalProps}
-      loading={loadingState === 'parsing'}
-      style={{ width: 800 }}
+      name="import-transactions"
+      isLoading={loadingState === 'parsing'}
+      containerProps={{ style: { width: 800 } }}
     >
-      {error && !error.parsed && (
-        <View style={{ alignItems: 'center', marginBottom: 15 }}>
-          <Text style={{ marginRight: 10, color: theme.errorText }}>
-            <strong>Error:</strong> {error.message}
-          </Text>
-        </View>
-      )}
-      {(!error || !error.parsed) && (
-        <View
-          style={{
-            flex: 'unset',
-            height: 300,
-            border: '1px solid ' + theme.tableBorder,
-          }}
-        >
-          <TableHeader headers={headers} />
-
-          <TableWithNavigator
-            items={transactions.filter(
-              trans =>
-                !trans.isMatchedTransaction ||
-                (trans.isMatchedTransaction && reconcile),
-            )}
-            fields={['payee', 'category', 'amount']}
-            style={{ backgroundColor: theme.tableHeaderBackground }}
-            getItemKey={index => index}
-            renderEmpty={() => {
-              return (
-                <View
-                  style={{
-                    textAlign: 'center',
-                    marginTop: 25,
-                    color: theme.tableHeaderText,
-                    fontStyle: 'italic',
-                  }}
-                >
-                  No transactions found
-                </View>
-              );
-            }}
-            renderItem={({ key, style, item }) => (
-              <View key={key} style={style}>
-                <Transaction
-                  transaction={item}
-                  showParsed={filetype === 'csv' || filetype === 'qif'}
-                  parseDateFormat={parseDateFormat}
-                  dateFormat={dateFormat}
-                  fieldMappings={fieldMappings}
-                  splitMode={splitMode}
-                  inOutMode={inOutMode}
-                  outValue={outValue}
-                  flipAmount={flipAmount}
-                  multiplierAmount={multiplierAmount}
-                  categories={categories.list}
-                  onCheckTransaction={onCheckTransaction}
-                  reconcile={reconcile}
-                />
-              </View>
-            )}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={
+              'Import transactions' +
+              (filetype ? ` (${filetype.toUpperCase()})` : '')
+            }
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-      )}
-      {error && error.parsed && (
-        <View
-          style={{
-            color: theme.errorText,
-            alignItems: 'center',
-            marginTop: 10,
-          }}
-        >
-          <Text style={{ maxWidth: 450, marginBottom: 15 }}>
-            <strong>Error:</strong> {error.message}
-          </Text>
-          {error.parsed && (
-            <Button onPress={() => onNewFile()}>Select new file...</Button>
+          {error && !error.parsed && (
+            <View style={{ alignItems: 'center', marginBottom: 15 }}>
+              <Text style={{ marginRight: 10, color: theme.errorText }}>
+                <strong>Error:</strong> {error.message}
+              </Text>
+            </View>
           )}
-        </View>
-      )}
-
-      {filetype === 'csv' && (
-        <View style={{ marginTop: 10 }}>
-          <FieldMappings
-            transactions={transactions}
-            onChange={onUpdateFields}
-            mappings={fieldMappings}
-            splitMode={splitMode}
-            inOutMode={inOutMode}
-            hasHeaderRow={hasHeaderRow}
-          />
-        </View>
-      )}
-
-      {isOfxFile(filetype) && (
-        <CheckboxOption
-          id="form_fallback_missing_payee"
-          checked={fallbackMissingPayeeToMemo}
-          onChange={() => {
-            setFallbackMissingPayeeToMemo(state => !state);
-            parse(
-              filename,
-              getParseOptions('ofx', {
-                fallbackMissingPayeeToMemo: !fallbackMissingPayeeToMemo,
-              }),
-            );
-          }}
-        >
-          Use Memo as a fallback for empty Payees
-        </CheckboxOption>
-      )}
-      {(isOfxFile(filetype) || isCamtFile(filetype)) && (
-        <CheckboxOption
-          id="form_dont_reconcile"
-          checked={reconcile}
-          onChange={() => {
-            setReconcile(!reconcile);
-          }}
-        >
-          Merge with existing transactions
-        </CheckboxOption>
-      )}
-
-      {/*Import Options */}
-      {(filetype === 'qif' || filetype === 'csv') && (
-        <View style={{ marginTop: 10 }}>
-          <Stack
-            direction="row"
-            align="flex-start"
-            spacing={1}
-            style={{ marginTop: 5 }}
-          >
-            {/*Date Format */}
-            <View>
-              {(filetype === 'qif' || filetype === 'csv') && (
-                <DateFormatSelect
-                  transactions={transactions}
-                  fieldMappings={fieldMappings}
-                  parseDateFormat={parseDateFormat}
-                  onChange={value => {
-                    setParseDateFormat(value);
-                    runImportPreview();
-                  }}
-                />
+          {(!error || !error.parsed) && (
+            <View
+              style={{
+                flex: 'unset',
+                height: 300,
+                border: '1px solid ' + theme.tableBorder,
+              }}
+            >
+              <TableHeader headers={headers} />
+
+              <TableWithNavigator
+                items={transactions.filter(
+                  trans =>
+                    !trans.isMatchedTransaction ||
+                    (trans.isMatchedTransaction && reconcile),
+                )}
+                fields={['payee', 'category', 'amount']}
+                style={{ backgroundColor: theme.tableHeaderBackground }}
+                getItemKey={index => index}
+                renderEmpty={() => {
+                  return (
+                    <View
+                      style={{
+                        textAlign: 'center',
+                        marginTop: 25,
+                        color: theme.tableHeaderText,
+                        fontStyle: 'italic',
+                      }}
+                    >
+                      No transactions found
+                    </View>
+                  );
+                }}
+                renderItem={({ key, style, item }) => (
+                  <View key={key} style={style}>
+                    <Transaction
+                      transaction={item}
+                      showParsed={filetype === 'csv' || filetype === 'qif'}
+                      parseDateFormat={parseDateFormat}
+                      dateFormat={dateFormat}
+                      fieldMappings={fieldMappings}
+                      splitMode={splitMode}
+                      inOutMode={inOutMode}
+                      outValue={outValue}
+                      flipAmount={flipAmount}
+                      multiplierAmount={multiplierAmount}
+                      categories={categories.list}
+                      onCheckTransaction={onCheckTransaction}
+                      reconcile={reconcile}
+                    />
+                  </View>
+                )}
+              />
+            </View>
+          )}
+          {error && error.parsed && (
+            <View
+              style={{
+                color: theme.errorText,
+                alignItems: 'center',
+                marginTop: 10,
+              }}
+            >
+              <Text style={{ maxWidth: 450, marginBottom: 15 }}>
+                <strong>Error:</strong> {error.message}
+              </Text>
+              {error.parsed && (
+                <Button onPress={() => onNewFile()}>Select new file...</Button>
               )}
             </View>
+          )}
 
-            {/* CSV Options */}
-            {filetype === 'csv' && (
-              <View style={{ marginLeft: 10, gap: 5 }}>
-                <SectionLabel title="CSV OPTIONS" />
-                <label
-                  style={{
-                    display: 'flex',
-                    flexDirection: 'row',
-                    gap: 5,
-                    alignItems: 'baseline',
-                  }}
-                >
-                  Delimiter:
-                  <Select
-                    options={[
-                      [',', ','],
-                      [';', ';'],
-                      ['|', '|'],
-                      ['\t', 'tab'],
-                    ]}
-                    value={delimiter}
-                    onChange={value => {
-                      setDelimiter(value);
-                      parse(
-                        filename,
-                        getParseOptions('csv', {
-                          delimiter: value,
-                          hasHeaderRow,
-                        }),
-                      );
-                    }}
-                    style={{ width: 50 }}
-                  />
-                </label>
-                <CheckboxOption
-                  id="form_has_header"
-                  checked={hasHeaderRow}
-                  onChange={() => {
-                    setHasHeaderRow(!hasHeaderRow);
-                    parse(
-                      filename,
-                      getParseOptions('csv', {
-                        delimiter,
-                        hasHeaderRow: !hasHeaderRow,
-                      }),
-                    );
-                  }}
-                >
-                  File has header row
-                </CheckboxOption>
-                <CheckboxOption
-                  id="clear_on_import"
-                  checked={clearOnImport}
-                  onChange={() => {
-                    setClearOnImport(!clearOnImport);
-                  }}
-                >
-                  Clear transactions on import
-                </CheckboxOption>
-                <CheckboxOption
-                  id="form_dont_reconcile"
-                  checked={reconcile}
-                  onChange={() => {
-                    setReconcile(!reconcile);
-                  }}
-                >
-                  Merge with existing transactions
-                </CheckboxOption>
-              </View>
-            )}
+          {filetype === 'csv' && (
+            <View style={{ marginTop: 10 }}>
+              <FieldMappings
+                transactions={transactions}
+                onChange={onUpdateFields}
+                mappings={fieldMappings}
+                splitMode={splitMode}
+                inOutMode={inOutMode}
+                hasHeaderRow={hasHeaderRow}
+              />
+            </View>
+          )}
 
-            <View style={{ flex: 1 }} />
-
-            <View style={{ marginRight: 10, gap: 5 }}>
-              <SectionLabel title="AMOUNT OPTIONS" />
-              <CheckboxOption
-                id="form_flip"
-                checked={flipAmount}
-                disabled={splitMode || inOutMode}
-                onChange={() => {
-                  setFlipAmount(!flipAmount);
-                  runImportPreview();
-                }}
+          {isOfxFile(filetype) && (
+            <CheckboxOption
+              id="form_fallback_missing_payee"
+              checked={fallbackMissingPayeeToMemo}
+              onChange={() => {
+                setFallbackMissingPayeeToMemo(state => !state);
+                parse(
+                  filename,
+                  getParseOptions('ofx', {
+                    fallbackMissingPayeeToMemo: !fallbackMissingPayeeToMemo,
+                  }),
+                );
+              }}
+            >
+              Use Memo as a fallback for empty Payees
+            </CheckboxOption>
+          )}
+          {(isOfxFile(filetype) || isCamtFile(filetype)) && (
+            <CheckboxOption
+              id="form_dont_reconcile"
+              checked={reconcile}
+              onChange={() => {
+                setReconcile(!reconcile);
+              }}
+            >
+              Merge with existing transactions
+            </CheckboxOption>
+          )}
+
+          {/*Import Options */}
+          {(filetype === 'qif' || filetype === 'csv') && (
+            <View style={{ marginTop: 10 }}>
+              <Stack
+                direction="row"
+                align="flex-start"
+                spacing={1}
+                style={{ marginTop: 5 }}
               >
-                Flip amount
-              </CheckboxOption>
-              {filetype === 'csv' && (
-                <>
+                {/*Date Format */}
+                <View>
+                  {(filetype === 'qif' || filetype === 'csv') && (
+                    <DateFormatSelect
+                      transactions={transactions}
+                      fieldMappings={fieldMappings}
+                      parseDateFormat={parseDateFormat}
+                      onChange={value => {
+                        setParseDateFormat(value);
+                        runImportPreview();
+                      }}
+                    />
+                  )}
+                </View>
+
+                {/* CSV Options */}
+                {filetype === 'csv' && (
+                  <View style={{ marginLeft: 10, gap: 5 }}>
+                    <SectionLabel title="CSV OPTIONS" />
+                    <label
+                      style={{
+                        display: 'flex',
+                        flexDirection: 'row',
+                        gap: 5,
+                        alignItems: 'baseline',
+                      }}
+                    >
+                      Delimiter:
+                      <Select
+                        options={[
+                          [',', ','],
+                          [';', ';'],
+                          ['|', '|'],
+                          ['\t', 'tab'],
+                        ]}
+                        value={delimiter}
+                        onChange={value => {
+                          setDelimiter(value);
+                          parse(
+                            filename,
+                            getParseOptions('csv', {
+                              delimiter: value,
+                              hasHeaderRow,
+                            }),
+                          );
+                        }}
+                        style={{ width: 50 }}
+                      />
+                    </label>
+                    <CheckboxOption
+                      id="form_has_header"
+                      checked={hasHeaderRow}
+                      onChange={() => {
+                        setHasHeaderRow(!hasHeaderRow);
+                        parse(
+                          filename,
+                          getParseOptions('csv', {
+                            delimiter,
+                            hasHeaderRow: !hasHeaderRow,
+                          }),
+                        );
+                      }}
+                    >
+                      File has header row
+                    </CheckboxOption>
+                    <CheckboxOption
+                      id="clear_on_import"
+                      checked={clearOnImport}
+                      onChange={() => {
+                        setClearOnImport(!clearOnImport);
+                      }}
+                    >
+                      Clear transactions on import
+                    </CheckboxOption>
+                    <CheckboxOption
+                      id="form_dont_reconcile"
+                      checked={reconcile}
+                      onChange={() => {
+                        setReconcile(!reconcile);
+                      }}
+                    >
+                      Merge with existing transactions
+                    </CheckboxOption>
+                  </View>
+                )}
+
+                <View style={{ flex: 1 }} />
+
+                <View style={{ marginRight: 10, gap: 5 }}>
+                  <SectionLabel title="AMOUNT OPTIONS" />
                   <CheckboxOption
-                    id="form_split"
-                    checked={splitMode}
-                    disabled={inOutMode || flipAmount}
+                    id="form_flip"
+                    checked={flipAmount}
+                    disabled={splitMode || inOutMode}
                     onChange={() => {
-                      onSplitMode();
+                      setFlipAmount(!flipAmount);
                       runImportPreview();
                     }}
                   >
-                    Split amount into separate inflow/outflow columns
+                    Flip amount
                   </CheckboxOption>
-                  <InOutOption
-                    inOutMode={inOutMode}
-                    outValue={outValue}
-                    disabled={splitMode || flipAmount}
+                  {filetype === 'csv' && (
+                    <>
+                      <CheckboxOption
+                        id="form_split"
+                        checked={splitMode}
+                        disabled={inOutMode || flipAmount}
+                        onChange={() => {
+                          onSplitMode();
+                          runImportPreview();
+                        }}
+                      >
+                        Split amount into separate inflow/outflow columns
+                      </CheckboxOption>
+                      <InOutOption
+                        inOutMode={inOutMode}
+                        outValue={outValue}
+                        disabled={splitMode || flipAmount}
+                        onToggle={() => {
+                          setInOutMode(!inOutMode);
+                          runImportPreview();
+                        }}
+                        onChangeText={setOutValue}
+                      />
+                    </>
+                  )}
+                  <MultiplierOption
+                    multiplierEnabled={multiplierEnabled}
+                    multiplierAmount={multiplierAmount}
                     onToggle={() => {
-                      setInOutMode(!inOutMode);
+                      setMultiplierEnabled(!multiplierEnabled);
+                      setMultiplierAmount('');
                       runImportPreview();
                     }}
-                    onChangeText={setOutValue}
+                    onChangeAmount={onMultiplierChange}
                   />
-                </>
-              )}
-              <MultiplierOption
-                multiplierEnabled={multiplierEnabled}
-                multiplierAmount={multiplierAmount}
-                onToggle={() => {
-                  setMultiplierEnabled(!multiplierEnabled);
-                  setMultiplierAmount('');
-                  runImportPreview();
+                </View>
+              </Stack>
+            </View>
+          )}
+
+          <View style={{ flexDirection: 'row', marginTop: 5 }}>
+            {/*Submit Button */}
+            <View
+              style={{
+                alignSelf: 'flex-end',
+                flexDirection: 'row',
+                alignItems: 'center',
+                gap: '1em',
+              }}
+            >
+              <ButtonWithLoading
+                variant="primary"
+                isDisabled={
+                  transactions?.filter(trans => !trans.isMatchedTransaction)
+                    .length === 0
+                }
+                isLoading={loadingState === 'importing'}
+                onPress={() => {
+                  onImport();
+                  close();
                 }}
-                onChangeAmount={onMultiplierChange}
-              />
+              >
+                Import{' '}
+                {
+                  transactions?.filter(trans => !trans.isMatchedTransaction)
+                    .length
+                }{' '}
+                transactions
+              </ButtonWithLoading>
             </View>
-          </Stack>
-        </View>
+          </View>
+        </>
       )}
-
-      <View style={{ flexDirection: 'row', marginTop: 5 }}>
-        {/*Submit Button */}
-        <View
-          style={{
-            alignSelf: 'flex-end',
-            flexDirection: 'row',
-            alignItems: 'center',
-            gap: '1em',
-          }}
-        >
-          <ButtonWithLoading
-            variant="primary"
-            isDisabled={
-              transactions?.filter(trans => !trans.isMatchedTransaction)
-                .length === 0
-            }
-            isLoading={loadingState === 'importing'}
-            onPress={onImport}
-          >
-            Import{' '}
-            {transactions?.filter(trans => !trans.isMatchedTransaction).length}{' '}
-            transactions
-          </ButtonWithLoading>
-        </View>
-      </View>
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx
index 5a4a3a0b1203f9a81942bad5624d01f603b826a5..0860c39ad05c15e476a7a6f74952fa9c75662635 100644
--- a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx
+++ b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx
@@ -3,14 +3,10 @@ import { useLocation } from 'react-router-dom';
 import * as Platform from 'loot-core/src/client/platform';
 
 import { type CSSProperties } from '../../style';
-import { Modal, type ModalProps } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 
-type KeyboardShortcutsModalProps = {
-  modalProps?: Partial<ModalProps>;
-};
-
 type KeyIconProps = {
   shortcut: string;
   style?: CSSProperties;
@@ -152,156 +148,191 @@ function Shortcut({
   );
 }
 
-export function KeyboardShortcutModal({
-  modalProps,
-}: KeyboardShortcutsModalProps) {
+export function KeyboardShortcutModal() {
   const location = useLocation();
   const onBudget = location.pathname.startsWith('/budget');
   const onAccounts = location.pathname.startsWith('/accounts');
   const ctrl = Platform.OS === 'mac' ? '⌘' : 'Ctrl';
   return (
-    <Modal title="Keyboard Shortcuts" {...modalProps}>
-      <View
-        style={{
-          flexDirection: 'row',
-          fontSize: 13,
-        }}
-      >
-        <View>
-          <Shortcut shortcut="?" description="Show this help dialog" />
-          <Shortcut
-            shortcut="O"
-            description="Close the current budget and open another"
-            meta={ctrl}
-          />
-          <Shortcut
-            shortcut="P"
-            description="Toggle the privacy filter"
-            meta={ctrl}
-            shift={true}
+    <Modal name="keyboard-shortcuts">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Keyboard Shortcuts"
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-          {onBudget && (
-            <Shortcut
-              shortcut="0"
-              description="View current month"
-              style={{
-                fontVariantNumeric: 'slashed-zero',
-              }}
-            />
-          )}
-          {onAccounts && (
-            <>
-              <Shortcut shortcut="Enter" description="Move down when editing" />
+          <View
+            style={{
+              flexDirection: 'row',
+              fontSize: 13,
+            }}
+          >
+            <View>
+              <Shortcut shortcut="?" description="Show this help dialog" />
               <Shortcut
-                shortcut="Enter"
-                description="Move up when editing"
-                shift={true}
-              />
-              <Shortcut
-                shortcut="I"
-                description="Import transactions"
+                shortcut="O"
+                description="Close the current budget and open another"
                 meta={ctrl}
               />
-              <Shortcut shortcut="B" description="Bank sync" meta={ctrl} />
-              <GroupHeading group="Select a transaction, then" />
-              <Shortcut
-                shortcut="J"
-                description="Move to the next transaction down"
-              />
-              <Shortcut
-                shortcut="K"
-                description="Move to the next transaction up"
-              />
-              <Shortcut
-                shortcut="↑"
-                description="Move to the next transaction down and scroll"
-              />
-              <Shortcut
-                shortcut="↓"
-                description="Move to the next transaction up and scroll"
-              />
               <Shortcut
-                shortcut="Space"
-                description="Toggle selection of current transaction"
-              />
-              <Shortcut
-                shortcut="Space"
-                description="Toggle all transactions between current and most recently selected transaction"
+                shortcut="P"
+                description="Toggle the privacy filter"
+                meta={ctrl}
                 shift={true}
               />
-            </>
-          )}
-        </View>
-        <View
-          style={{
-            marginRight: 15,
-          }}
-        >
-          <Shortcut
-            shortcut="Z"
-            description="Undo the last change"
-            meta={ctrl}
-          />
-          <Shortcut
-            shortcut="Z"
-            description="Redo the last undone change"
-            shift={true}
-            meta={ctrl}
-          />
-          {onBudget && (
-            <>
-              <Shortcut shortcut="←" description="View previous month" />
-              <Shortcut shortcut="→" description="View next month" />
-            </>
-          )}
-          {onAccounts && (
-            <>
+              {onBudget && (
+                <Shortcut
+                  shortcut="0"
+                  description="View current month"
+                  style={{
+                    fontVariantNumeric: 'slashed-zero',
+                  }}
+                />
+              )}
+              {onAccounts && (
+                <>
+                  <Shortcut
+                    shortcut="Enter"
+                    description="Move down when editing"
+                  />
+                  <Shortcut
+                    shortcut="Enter"
+                    description="Move up when editing"
+                    shift={true}
+                  />
+                  <Shortcut
+                    shortcut="I"
+                    description="Import transactions"
+                    meta={ctrl}
+                  />
+                  <Shortcut shortcut="B" description="Bank sync" meta={ctrl} />
+                  <GroupHeading group="Select a transaction, then" />
+                  <Shortcut
+                    shortcut="J"
+                    description="Move to the next transaction down"
+                  />
+                  <Shortcut
+                    shortcut="K"
+                    description="Move to the next transaction up"
+                  />
+                  <Shortcut
+                    shortcut="↑"
+                    description="Move to the next transaction down and scroll"
+                  />
+                  <Shortcut
+                    shortcut="↓"
+                    description="Move to the next transaction up and scroll"
+                  />
+                  <Shortcut
+                    shortcut="Space"
+                    description="Toggle selection of current transaction"
+                  />
+                  <Shortcut
+                    shortcut="Space"
+                    description="Toggle all transactions between current and most recently selected transaction"
+                    shift={true}
+                  />
+                </>
+              )}
+            </View>
+            <View
+              style={{
+                marginRight: 15,
+              }}
+            >
               <Shortcut
-                shortcut="A"
-                description="Select all transactions"
+                shortcut="Z"
+                description="Undo the last change"
                 meta={ctrl}
               />
-              <Shortcut shortcut="Tab" description="Move right when editing" />
               <Shortcut
-                shortcut="Tab"
-                description="Move left when editing"
+                shortcut="Z"
+                description="Redo the last undone change"
                 shift={true}
+                meta={ctrl}
               />
-              <Shortcut shortcut="T" description="Add a new transaction" />
-              <Shortcut shortcut="F" description="Filter transactions" />
-              <GroupHeading group="With transaction(s) selected" />
-              <Shortcut
-                shortcut="F"
-                description="Filter to the selected transactions"
-              />
-              <Shortcut
-                shortcut="D"
-                description="Delete selected transactions"
-              />
-              <Shortcut
-                shortcut="A"
-                description="Set account for selected transactions"
-              />
-              <Shortcut
-                shortcut="P"
-                description="Set payee for selected transactions"
-              />
-              <Shortcut
-                shortcut="N"
-                description="Set notes for selected transactions"
-              />
-              <Shortcut
-                shortcut="C"
-                description="Set category for selected transactions"
-              />
-              <Shortcut
-                shortcut="L"
-                description="Toggle cleared for current transaction"
-              />
-            </>
-          )}
-        </View>
-      </View>
+              {onAccounts && (
+                <>
+                  <Shortcut
+                    shortcut="Enter"
+                    description="Move up when editing"
+                    shift={true}
+                  />
+                  <Shortcut
+                    shortcut="Tab"
+                    description="Move left when editing"
+                    shift={true}
+                  />
+                  {onBudget && (
+                    <>
+                      <Shortcut
+                        shortcut="←"
+                        description="View previous month"
+                      />
+                      <Shortcut shortcut="→" description="View next month" />
+                    </>
+                  )}
+                  {onAccounts && (
+                    <>
+                      <Shortcut
+                        shortcut="A"
+                        description="Select all transactions"
+                        meta={ctrl}
+                      />
+                      <Shortcut
+                        shortcut="Tab"
+                        description="Move right when editing"
+                      />
+                      <Shortcut
+                        shortcut="Tab"
+                        description="Move left when editing"
+                        shift={true}
+                      />
+                      <Shortcut
+                        shortcut="T"
+                        description="Add a new transaction"
+                      />
+                      <Shortcut
+                        shortcut="F"
+                        description="Filter transactions"
+                      />
+                      <GroupHeading group="With transaction(s) selected" />
+                      <Shortcut
+                        shortcut="F"
+                        description="Filter to the selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="D"
+                        description="Delete selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="A"
+                        description="Set account for selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="P"
+                        description="Set payee for selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="N"
+                        description="Set notes for selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="C"
+                        description="Set category for selected transactions"
+                      />
+                      <Shortcut
+                        shortcut="L"
+                        description="Toggle cleared for current transaction"
+                      />
+                    </>
+                  )}
+                </>
+              )}
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/LoadBackup.jsx b/packages/desktop-client/src/components/modals/LoadBackup.jsx
index 9ed813034a38d80f768d546f040c4a336702a7dc..21d750d07645866123b0d308e27a1475fc86d0bb 100644
--- a/packages/desktop-client/src/components/modals/LoadBackup.jsx
+++ b/packages/desktop-client/src/components/modals/LoadBackup.jsx
@@ -1,12 +1,14 @@
 import React, { Component, useState, useEffect } from 'react';
+import { useDispatch } from 'react-redux';
 
+import { loadBackup, makeBackup } from 'loot-core/client/actions';
 import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch';
 
 import { useLocalPref } from '../../hooks/useLocalPref';
 import { theme } from '../../style';
 import { Block } from '../common/Block';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { Row, Cell } from '../table';
@@ -48,13 +50,8 @@ class BackupTable extends Component {
   }
 }
 
-export function LoadBackup({
-  budgetId,
-  watchUpdates,
-  backupDisabled,
-  actions,
-  modalProps,
-}) {
+export function LoadBackup({ budgetId, watchUpdates, backupDisabled }) {
+  const dispatch = useDispatch();
   const [backups, setBackups] = useState([]);
   const [prefsBudgetId] = useLocalPref('id');
   const budgetIdToLoad = budgetId || prefsBudgetId;
@@ -74,66 +71,73 @@ export function LoadBackup({
   const previousBackups = backups.filter(backup => !backup.isLatest);
 
   return (
-    <Modal title="Load Backup" {...modalProps} style={{ flex: 0 }}>
-      {() => (
-        <View style={{ marginBottom: 30 }}>
-          <View
-            style={{
-              margin: 20,
-              marginTop: 0,
-              marginBottom: 15,
-              lineHeight: 1.5,
-            }}
-          >
-            {latestBackup ? (
-              <Block>
-                <Block style={{ marginBottom: 10 }}>
-                  <Text style={{ fontWeight: 600 }}>
-                    You are currently working from a backup.
-                  </Text>{' '}
-                  You can load a different backup or revert to the original
-                  version below.
+    <Modal name="load-backup" containerProps={{ style: { maxWidth: '30vw' } }}>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Load Backup"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View style={{ marginBottom: 30 }}>
+            <View
+              style={{
+                margin: 20,
+                marginTop: 0,
+                marginBottom: 15,
+                lineHeight: 1.5,
+              }}
+            >
+              {latestBackup ? (
+                <Block>
+                  <Block style={{ marginBottom: 10 }}>
+                    <Text style={{ fontWeight: 600 }}>
+                      You are currently working from a backup.
+                    </Text>{' '}
+                    You can load a different backup or revert to the original
+                    version below.
+                  </Block>
+                  <Button
+                    variant="primary"
+                    onPress={() =>
+                      dispatch(loadBackup(budgetIdToLoad, latestBackup.id))
+                    }
+                  >
+                    Revert to original version
+                  </Button>
                 </Block>
-                <Button
-                  variant="primary"
-                  onPress={() =>
-                    actions.loadBackup(budgetIdToLoad, latestBackup.id)
-                  }
-                >
-                  Revert to original version
-                </Button>
+              ) : (
+                <View style={{ alignItems: 'flex-start' }}>
+                  <Block style={{ marginBottom: 10 }}>
+                    Select a backup to load. After loading a backup, you will
+                    have a chance to revert to the current version in this
+                    screen.{' '}
+                    <Text style={{ fontWeight: 600 }}>
+                      If you use a backup, you will have to setup all your
+                      devices to sync from the new budget.
+                    </Text>
+                  </Block>
+                  <Button
+                    variant="primary"
+                    isDisabled={backupDisabled}
+                    onPress={() => dispatch(makeBackup())}
+                  >
+                    Backup now
+                  </Button>
+                </View>
+              )}
+            </View>
+            {previousBackups.length === 0 ? (
+              <Block style={{ color: theme.tableTextLight, marginLeft: 20 }}>
+                No backups available
               </Block>
             ) : (
-              <View style={{ alignItems: 'flex-start' }}>
-                <Block style={{ marginBottom: 10 }}>
-                  Select a backup to load. After loading a backup, you will have
-                  a chance to revert to the current version in this screen.{' '}
-                  <Text style={{ fontWeight: 600 }}>
-                    If you use a backup, you will have to setup all your devices
-                    to sync from the new budget.
-                  </Text>
-                </Block>
-                <Button
-                  variant="primary"
-                  isDisabled={backupDisabled}
-                  onPress={() => actions.makeBackup()}
-                >
-                  Backup now
-                </Button>
-              </View>
+              <BackupTable
+                backups={previousBackups}
+                onSelect={id => dispatch(loadBackup(budgetIdToLoad, id))}
+              />
             )}
           </View>
-          {previousBackups.length === 0 ? (
-            <Block style={{ color: theme.tableTextLight, marginLeft: 20 }}>
-              No backups available
-            </Block>
-          ) : (
-            <BackupTable
-              backups={previousBackups}
-              onSelect={id => actions.loadBackup(budgetIdToLoad, id)}
-            />
-          )}
-        </View>
+        </>
       )}
     </Modal>
   );
diff --git a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx
index 9d1a690222ec4d9b402e836985f669056dbdf158..898c9992a2aeffa85f2dc9cda97460f6f21e30ce 100644
--- a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx
+++ b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx
@@ -4,19 +4,14 @@ import { useLocation } from 'react-router-dom';
 
 import { isNonProductionEnvironment } from 'loot-core/src/shared/environment';
 
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { ManageRules } from '../ManageRules';
-import { type CommonModalProps } from '../Modals';
 
 type ManageRulesModalProps = {
-  modalProps: CommonModalProps;
   payeeId?: string;
 };
 
-export function ManageRulesModal({
-  modalProps,
-  payeeId,
-}: ManageRulesModalProps) {
+export function ManageRulesModal({ payeeId }: ManageRulesModalProps) {
   const [loading, setLoading] = useState(true);
   const location = useLocation();
   if (isNonProductionEnvironment()) {
@@ -28,8 +23,16 @@ export function ManageRulesModal({
   }
 
   return (
-    <Modal title="Rules" loading={loading} {...modalProps}>
-      {() => <ManageRules isModal payeeId={payeeId} setLoading={setLoading} />}
+    <Modal name="manage-rules" isLoading={loading}>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Rules"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <ManageRules isModal payeeId={payeeId} setLoading={setLoading} />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
index fc1535a8953da96df50cb523e193a8daad88da1b..2a82568695ad9667f3bb3ddf6d9f1c58b8566a48 100644
--- a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
+++ b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx
@@ -8,14 +8,14 @@ import { usePayees } from '../../hooks/usePayees';
 import { theme } from '../../style';
 import { Information } from '../alerts';
 import { Button } from '../common/Button2';
-import { Modal, ModalButtons } from '../common/Modal';
+import { Modal, ModalButtons } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 
 const highlightStyle = { color: theme.pageTextPositive };
 
-export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) {
+export function MergeUnusedPayees({ payeeIds, targetPayeeId }) {
   const allPayees = usePayees();
   const modalStack = useSelector(state => state.modals.modalStack);
   const isEditingRule = !!modalStack.find(m => m.name === 'edit-rule');
@@ -63,8 +63,6 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) {
       ruleId = id;
     }
 
-    modalProps.onClose();
-
     return ruleId;
   }
 
@@ -78,13 +76,8 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) {
   }
 
   return (
-    <Modal
-      title="Merge payee?"
-      showHeader={false}
-      {...modalProps}
-      style={modalProps.style}
-    >
-      {() => (
+    <Modal name="merge-unused-payees">
+      {({ state: { close } }) => (
         <View style={{ padding: 20, maxWidth: 500 }}>
           <View>
             <Paragraph style={{ marginBottom: 10, fontWeight: 500 }}>
@@ -159,22 +152,25 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) {
               <Button
                 variant="primary"
                 style={{ marginRight: 10 }}
-                onPress={onMerge}
+                onPress={() => {
+                  onMerge();
+                  close();
+                }}
               >
                 Merge
               </Button>
               {!isEditingRule && (
                 <Button
                   style={{ marginRight: 10 }}
-                  onPress={onMergeAndCreateRule}
+                  onPress={() => {
+                    onMergeAndCreateRule();
+                    close();
+                  }}
                 >
                   Merge and edit rule
                 </Button>
               )}
-              <Button
-                style={{ marginRight: 10 }}
-                onPress={() => modalProps.onBack()}
-              >
+              <Button style={{ marginRight: 10 }} onPress={close}>
                 Do nothing
               </Button>
             </ModalButtons>
diff --git a/packages/desktop-client/src/components/modals/NotesModal.tsx b/packages/desktop-client/src/components/modals/NotesModal.tsx
index 04bd358237f450fcb9e004a1a5e737df42192d38..1ef3e00ad0fd819343130d1278ef369dc573210d 100644
--- a/packages/desktop-client/src/components/modals/NotesModal.tsx
+++ b/packages/desktop-client/src/components/modals/NotesModal.tsx
@@ -4,87 +4,86 @@ import React, { useEffect, useState } from 'react';
 import { useNotes } from '../../hooks/useNotes';
 import { SvgCheck } from '../../icons/v2';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type NotesModalProps = {
-  modalProps: CommonModalProps;
   id: string;
   name: string;
   onSave: (id: string, notes: string) => void;
 };
 
-export function NotesModal({ modalProps, id, name, onSave }: NotesModalProps) {
+export function NotesModal({ id, name, onSave }: NotesModalProps) {
   const originalNotes = useNotes(id);
 
   const [notes, setNotes] = useState(originalNotes);
   useEffect(() => setNotes(originalNotes), [originalNotes]);
 
-  function _onClose() {
-    modalProps?.onClose();
-  }
-
   function _onSave() {
     if (notes !== originalNotes) {
       onSave?.(id, notes);
     }
-
-    _onClose();
   }
 
   return (
     <Modal
-      title={`Notes: ${name}`}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: '50vh',
+      name="notes"
+      containerProps={{
+        style: { height: '50vh' },
       }}
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <Notes
-          notes={notes}
-          editable={true}
-          focused={true}
-          getStyle={() => ({
-            borderRadius: 6,
-            flex: 1,
-            minWidth: 0,
-          })}
-          onChange={setNotes}
-        />
-        <View
-          style={{
-            flexDirection: 'column',
-            alignItems: 'center',
-            justifyItems: 'center',
-            width: '100%',
-            paddingTop: 10,
-          }}
-        >
-          <Button
-            variant="primary"
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={`Notes: ${name}`}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
             style={{
-              fontSize: 17,
-              fontWeight: 400,
-              width: '100%',
+              flex: 1,
+              flexDirection: 'column',
             }}
-            onPress={_onSave}
           >
-            <SvgCheck width={17} height={17} style={{ paddingRight: 5 }} />
-            Save notes
-          </Button>
-        </View>
-      </View>
+            <Notes
+              notes={notes}
+              editable={true}
+              focused={true}
+              getStyle={() => ({
+                borderRadius: 6,
+                flex: 1,
+                minWidth: 0,
+              })}
+              onChange={setNotes}
+            />
+            <View
+              style={{
+                flexDirection: 'column',
+                alignItems: 'center',
+                justifyItems: 'center',
+                width: '100%',
+                paddingTop: 10,
+              }}
+            >
+              <Button
+                variant="primary"
+                style={{
+                  fontSize: 17,
+                  fontWeight: 400,
+                  width: '100%',
+                }}
+                onPress={() => {
+                  _onSave();
+                  close();
+                }}
+              >
+                <SvgCheck width={17} height={17} style={{ paddingRight: 5 }} />
+                Save notes
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx
index 952361cacc735f1186bde7537e817853d1f6b3c3..9ab0dc1d166358b4b3e8ee5fc7faf6fb2991725f 100644
--- a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx
+++ b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx
@@ -6,17 +6,19 @@ import { usePayees } from '../../hooks/usePayees';
 import { useResponsive } from '../../ResponsiveProvider';
 import { theme } from '../../style';
 import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete';
-import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal';
-import { type CommonModalProps } from '../Modals';
+import {
+  ModalCloseButton,
+  Modal,
+  ModalTitle,
+  ModalHeader,
+} from '../common/Modal2';
 
 type PayeeAutocompleteModalProps = {
-  modalProps: CommonModalProps;
   autocompleteProps: ComponentPropsWithoutRef<typeof PayeeAutocomplete>;
   onClose: () => void;
 };
 
 export function PayeeAutocompleteModal({
-  modalProps,
   autocompleteProps,
   onClose,
 }: PayeeAutocompleteModalProps) {
@@ -24,11 +26,6 @@ export function PayeeAutocompleteModal({
   const accounts = useAccounts() || [];
   const navigate = useNavigate();
 
-  const _onClose = () => {
-    modalProps.onClose();
-    onClose?.();
-  };
-
   const { isNarrowWidth } = useResponsive();
   const defaultAutocompleteProps = {
     containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } },
@@ -38,41 +35,49 @@ export function PayeeAutocompleteModal({
 
   return (
     <Modal
-      title={
-        <ModalTitle
-          title="Payee"
-          getStyle={() => ({ color: theme.menuAutoCompleteText })}
-        />
-      }
+      name="payee-autocomplete"
       noAnimation={!isNarrowWidth}
-      showHeader={isNarrowWidth}
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={_onClose}
-      style={{
-        height: isNarrowWidth ? '85vh' : 275,
-        backgroundColor: theme.menuAutoCompleteBackground,
+      onClose={onClose}
+      containerProps={{
+        style: {
+          height: isNarrowWidth ? '85vh' : 275,
+          backgroundColor: theme.menuAutoCompleteBackground,
+        },
       }}
-      CloseButton={props => (
-        <ModalCloseButton
-          {...props}
-          style={{ color: theme.menuAutoCompleteText }}
-        />
-      )}
     >
-      <PayeeAutocomplete
-        payees={payees}
-        accounts={accounts}
-        focused={true}
-        embedded={true}
-        closeOnBlur={false}
-        onClose={_onClose}
-        onManagePayees={onManagePayees}
-        showManagePayees={!isNarrowWidth}
-        showMakeTransfer={!isNarrowWidth}
-        {...defaultAutocompleteProps}
-        {...autocompleteProps}
-      />
+      {({ state: { close } }) => (
+        <>
+          {isNarrowWidth && (
+            <ModalHeader
+              title={
+                <ModalTitle
+                  title="Payee"
+                  getStyle={() => ({ color: theme.menuAutoCompleteText })}
+                />
+              }
+              rightContent={
+                <ModalCloseButton
+                  onClick={close}
+                  style={{ color: theme.menuAutoCompleteText }}
+                />
+              }
+            />
+          )}
+          <PayeeAutocomplete
+            payees={payees}
+            accounts={accounts}
+            focused={true}
+            embedded={true}
+            closeOnBlur={false}
+            onClose={close}
+            onManagePayees={onManagePayees}
+            showManagePayees={!isNarrowWidth}
+            showMakeTransfer={!isNarrowWidth}
+            {...defaultAutocompleteProps}
+            {...autocompleteProps}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx
index 9b624653479f8e39c77e948255be0fdb62165d34..01808eea13d8a351a6dc8bde588c70c4784188c4 100644
--- a/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx
@@ -9,19 +9,18 @@ import {
   DefaultCarryoverIndicator,
 } from '../budget/BalanceWithCarryover';
 import { BalanceMenu } from '../budget/report/BalanceMenu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
-type ReportBalanceMenuModalProps = ComponentPropsWithoutRef<
-  typeof BalanceMenu
-> & {
-  modalProps: CommonModalProps;
-};
+type ReportBalanceMenuModalProps = ComponentPropsWithoutRef<typeof BalanceMenu>;
 
 export function ReportBalanceMenuModal({
-  modalProps,
   categoryId,
   onCarryover,
 }: ReportBalanceMenuModalProps) {
@@ -39,55 +38,58 @@ export function ReportBalanceMenuModal({
   }
 
   return (
-    <Modal
-      title={<ModalTitle title={category.name} shrinkOnOverflow />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          marginBottom: 20,
-        }}
-      >
-        <Text
-          style={{
-            fontSize: 17,
-            fontWeight: 400,
-          }}
-        >
-          Balance
-        </Text>
-        <BalanceWithCarryover
-          disabled
-          style={{
-            textAlign: 'center',
-            ...styles.veryLargeText,
-          }}
-          carryover={reportBudget.catCarryover(categoryId)}
-          balance={reportBudget.catBalance(categoryId)}
-          goal={reportBudget.catGoal(categoryId)}
-          budgeted={reportBudget.catBudgeted(categoryId)}
-          carryoverIndicator={({ style }) =>
-            DefaultCarryoverIndicator({
-              style: {
-                width: 15,
-                height: 15,
-                display: 'inline-flex',
-                position: 'relative',
-                ...style,
-              },
-            })
-          }
-        />
-      </View>
-      <BalanceMenu
-        categoryId={categoryId}
-        getItemStyle={() => defaultMenuItemStyle}
-        onCarryover={onCarryover}
-      />
+    <Modal name="report-balance-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title={category.name} shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 20,
+            }}
+          >
+            <Text
+              style={{
+                fontSize: 17,
+                fontWeight: 400,
+              }}
+            >
+              Balance
+            </Text>
+            <BalanceWithCarryover
+              disabled
+              style={{
+                textAlign: 'center',
+                ...styles.veryLargeText,
+              }}
+              carryover={reportBudget.catCarryover(categoryId)}
+              balance={reportBudget.catBalance(categoryId)}
+              goal={reportBudget.catGoal(categoryId)}
+              budgeted={reportBudget.catBudgeted(categoryId)}
+              carryoverIndicator={({ style }) =>
+                DefaultCarryoverIndicator({
+                  style: {
+                    width: 15,
+                    height: 15,
+                    display: 'inline-flex',
+                    position: 'relative',
+                    ...style,
+                  },
+                })
+              }
+            />
+          </View>
+          <BalanceMenu
+            categoryId={categoryId}
+            getItemStyle={() => defaultMenuItemStyle}
+            onCarryover={onCarryover}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx
index 13ac4bd665764394cc79fee38c67b410d5937c4a..b28976d6379e69cb0c295e6458cdaf0e3c8517f7 100644
--- a/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx
@@ -10,23 +10,25 @@ import { amountToInteger, integerToAmount } from 'loot-core/shared/util';
 import { useCategory } from '../../hooks/useCategory';
 import { type CSSProperties, theme, styles } from '../../style';
 import { BudgetMenu } from '../budget/report/BudgetMenu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput';
-import { type CommonModalProps } from '../Modals';
 import { useSheetValue } from '../spreadsheet/useSheetValue';
 
 type ReportBudgetMenuModalProps = ComponentPropsWithoutRef<
   typeof BudgetMenu
 > & {
-  modalProps: CommonModalProps;
   categoryId: string;
   onUpdateBudget: (amount: number) => void;
 };
 
 export function ReportBudgetMenuModal({
-  modalProps,
   categoryId,
   onUpdateBudget,
   onCopyLastMonthAverage,
@@ -57,51 +59,54 @@ export function ReportBudgetMenuModal({
   }
 
   return (
-    <Modal
-      title={<ModalTitle title={category.name} shrinkOnOverflow />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          marginBottom: 20,
-        }}
-      >
-        <Text
-          style={{
-            fontSize: 17,
-            fontWeight: 400,
-          }}
-        >
-          Budget
-        </Text>
-        <FocusableAmountInput
-          value={integerToAmount(budgeted || 0)}
-          focused={amountFocused}
-          onFocus={() => setAmountFocused(true)}
-          onBlur={() => setAmountFocused(false)}
-          onEnter={() => modalProps.onClose()}
-          zeroSign="+"
-          focusedStyle={{
-            width: 'auto',
-            padding: '5px',
-            paddingLeft: '20px',
-            paddingRight: '20px',
-            minWidth: '100%',
-          }}
-          textStyle={{ ...styles.veryLargeText, textAlign: 'center' }}
-          onUpdateAmount={_onUpdateBudget}
-        />
-      </View>
-      <BudgetMenu
-        getItemStyle={() => defaultMenuItemStyle}
-        onCopyLastMonthAverage={onCopyLastMonthAverage}
-        onSetMonthsAverage={onSetMonthsAverage}
-        onApplyBudgetTemplate={onApplyBudgetTemplate}
-      />
+    <Modal name="report-budget-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title={category.name} shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 20,
+            }}
+          >
+            <Text
+              style={{
+                fontSize: 17,
+                fontWeight: 400,
+              }}
+            >
+              Budget
+            </Text>
+            <FocusableAmountInput
+              value={integerToAmount(budgeted || 0)}
+              focused={amountFocused}
+              onFocus={() => setAmountFocused(true)}
+              onBlur={() => setAmountFocused(false)}
+              onEnter={close}
+              zeroSign="+"
+              focusedStyle={{
+                width: 'auto',
+                padding: '5px',
+                paddingLeft: '20px',
+                paddingRight: '20px',
+                minWidth: '100%',
+              }}
+              textStyle={{ ...styles.veryLargeText, textAlign: 'center' }}
+              onUpdateAmount={_onUpdateBudget}
+            />
+          </View>
+          <BudgetMenu
+            getItemStyle={() => defaultMenuItemStyle}
+            onCopyLastMonthAverage={onCopyLastMonthAverage}
+            onSetMonthsAverage={onSetMonthsAverage}
+            onApplyBudgetTemplate={onApplyBudgetTemplate}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx
index 72eee1de25175895623a6a09a4a4bfd5b1529673..4c996dfc0afd00704318e06a9b6ed9420e57454a 100644
--- a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx
@@ -9,30 +9,23 @@ import { SvgNotesPaper } from '../../icons/v2';
 import { type CSSProperties, styles, theme } from '../../style';
 import { BudgetMonthMenu } from '../budget/report/budgetsummary/BudgetMonthMenu';
 import { Button } from '../common/Button2';
-import { Modal, ModalTitle } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type ReportBudgetMonthMenuModalProps = {
-  modalProps: CommonModalProps;
   month: string;
   onBudgetAction: (month: string, action: string, arg?: unknown) => void;
   onEditNotes: (month: string) => void;
 };
 
 export function ReportBudgetMonthMenuModal({
-  modalProps,
   month,
   onBudgetAction,
   onEditNotes,
 }: ReportBudgetMonthMenuModalProps) {
   const originalNotes = useNotes(`budget-${month}`);
 
-  const onClose = () => {
-    modalProps.onClose();
-  };
-
   const _onEditNotes = () => {
     onEditNotes?.(month);
   };
@@ -60,120 +53,127 @@ export function ReportBudgetMonthMenuModal({
 
   return (
     <Modal
-      title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={onClose}
-      style={{
-        height: '50vh',
+      name="report-budget-month-menu"
+      containerProps={{
+        style: { height: '50vh' },
       }}
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <View
-          style={{
-            display: showMore ? 'none' : undefined,
-            overflowY: 'auto',
-            flex: 1,
-          }}
-        >
-          <Notes
-            notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
-            editable={false}
-            focused={false}
-            getStyle={() => ({
-              borderRadius: 6,
-              ...((!originalNotes || originalNotes.length === 0) && {
-                justifySelf: 'center',
-                alignSelf: 'center',
-                color: theme.pageTextSubdued,
-              }),
-            })}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={monthUtils.format(month, 'MMMM ‘yy')}
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-        <View style={{ paddingTop: 10, gap: 5 }}>
           <View
             style={{
-              display: showMore ? 'none' : undefined,
-              flexDirection: 'row',
-              flexWrap: 'wrap',
-              justifyContent: 'space-between',
-              alignContent: 'space-between',
+              flex: 1,
+              flexDirection: 'column',
             }}
           >
-            <Button style={buttonStyle} onPress={_onEditNotes}>
-              <SvgNotesPaper
-                width={20}
-                height={20}
-                style={{ paddingRight: 5 }}
-              />
-              Edit notes
-            </Button>
-          </View>
-          <View>
-            <Button
-              variant="bare"
-              style={({ isPressed, isHovered }) => ({
-                ...buttonStyle,
-                ...(isPressed || isHovered
-                  ? { backgroundColor: 'transparent', color: buttonStyle.color }
-                  : {}),
-              })}
-              onPress={onShowMore}
+            <View
+              style={{
+                display: showMore ? 'none' : undefined,
+                overflowY: 'auto',
+                flex: 1,
+              }}
             >
-              {!showMore ? (
-                <SvgCheveronUp
-                  width={30}
-                  height={30}
-                  style={{ paddingRight: 5 }}
-                />
-              ) : (
-                <SvgCheveronDown
-                  width={30}
-                  height={30}
-                  style={{ paddingRight: 5 }}
-                />
-              )}
-              Actions
-            </Button>
+              <Notes
+                notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
+                editable={false}
+                focused={false}
+                getStyle={() => ({
+                  borderRadius: 6,
+                  ...((!originalNotes || originalNotes.length === 0) && {
+                    justifySelf: 'center',
+                    alignSelf: 'center',
+                    color: theme.pageTextSubdued,
+                  }),
+                })}
+              />
+            </View>
+            <View style={{ paddingTop: 10, gap: 5 }}>
+              <View
+                style={{
+                  display: showMore ? 'none' : undefined,
+                  flexDirection: 'row',
+                  flexWrap: 'wrap',
+                  justifyContent: 'space-between',
+                  alignContent: 'space-between',
+                }}
+              >
+                <Button style={buttonStyle} onPress={_onEditNotes}>
+                  <SvgNotesPaper
+                    width={20}
+                    height={20}
+                    style={{ paddingRight: 5 }}
+                  />
+                  Edit notes
+                </Button>
+              </View>
+              <View>
+                <Button
+                  variant="bare"
+                  style={({ isPressed, isHovered }) => ({
+                    ...buttonStyle,
+                    ...(isPressed || isHovered
+                      ? {
+                          backgroundColor: 'transparent',
+                          color: buttonStyle.color,
+                        }
+                      : {}),
+                  })}
+                  onPress={onShowMore}
+                >
+                  {!showMore ? (
+                    <SvgCheveronUp
+                      width={30}
+                      height={30}
+                      style={{ paddingRight: 5 }}
+                    />
+                  ) : (
+                    <SvgCheveronDown
+                      width={30}
+                      height={30}
+                      style={{ paddingRight: 5 }}
+                    />
+                  )}
+                  Actions
+                </Button>
+              </View>
+            </View>
+            {showMore && (
+              <BudgetMonthMenu
+                style={{ overflowY: 'auto', paddingTop: 10 }}
+                getItemStyle={() => defaultMenuItemStyle}
+                onCopyLastMonthBudget={() => {
+                  onBudgetAction(month, 'copy-last');
+                  close();
+                }}
+                onSetBudgetsToZero={() => {
+                  onBudgetAction(month, 'set-zero');
+                  close();
+                }}
+                onSetMonthsAverage={numberOfMonths => {
+                  onBudgetAction(month, `set-${numberOfMonths}-avg`);
+                  close();
+                }}
+                onCheckTemplates={() => {
+                  onBudgetAction(month, 'check-templates');
+                  close();
+                }}
+                onApplyBudgetTemplates={() => {
+                  onBudgetAction(month, 'apply-goal-template');
+                  close();
+                }}
+                onOverwriteWithBudgetTemplates={() => {
+                  onBudgetAction(month, 'overwrite-goal-template');
+                  close();
+                }}
+              />
+            )}
           </View>
-        </View>
-        {showMore && (
-          <BudgetMonthMenu
-            style={{ overflowY: 'auto', paddingTop: 10 }}
-            getItemStyle={() => defaultMenuItemStyle}
-            onCopyLastMonthBudget={() => {
-              onBudgetAction(month, 'copy-last');
-              onClose();
-            }}
-            onSetBudgetsToZero={() => {
-              onBudgetAction(month, 'set-zero');
-              onClose();
-            }}
-            onSetMonthsAverage={numberOfMonths => {
-              onBudgetAction(month, `set-${numberOfMonths}-avg`);
-              onClose();
-            }}
-            onCheckTemplates={() => {
-              onBudgetAction(month, 'check-templates');
-              onClose();
-            }}
-            onApplyBudgetTemplates={() => {
-              onBudgetAction(month, 'apply-goal-template');
-              onClose();
-            }}
-            onOverwriteWithBudgetTemplates={() => {
-              onBudgetAction(month, 'overwrite-goal-template');
-              onClose();
-            }}
-          />
-        )}
-      </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx
index 144da8fe02cdf44e4ff52a703b95c5209b86627f..7515dec21e20ff04571b56bec38787c99547a64a 100644
--- a/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx
+++ b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx
@@ -7,40 +7,45 @@ import { styles } from '../../style';
 import { ExpenseTotal } from '../budget/report/budgetsummary/ExpenseTotal';
 import { IncomeTotal } from '../budget/report/budgetsummary/IncomeTotal';
 import { Saved } from '../budget/report/budgetsummary/Saved';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Stack } from '../common/Stack';
-import { type CommonModalProps } from '../Modals';
 import { NamespaceContext } from '../spreadsheet/NamespaceContext';
 
 type ReportBudgetSummaryModalProps = {
-  modalProps: CommonModalProps;
   month: string;
 };
 
 export function ReportBudgetSummaryModal({
   month,
-  modalProps,
 }: ReportBudgetSummaryModalProps) {
   const currentMonth = monthUtils.currentMonth();
   return (
-    <Modal title="Budget Summary" {...modalProps}>
-      <NamespaceContext.Provider value={sheetForMonth(month)}>
-        <Stack
-          spacing={2}
-          style={{
-            alignSelf: 'center',
-            backgroundColor: 'transparent',
-            borderRadius: 4,
-          }}
-        >
-          <IncomeTotal style={{ ...styles.mediumText }} />
-          <ExpenseTotal style={{ ...styles.mediumText }} />
-        </Stack>
-        <Saved
-          projected={month >= currentMonth}
-          style={{ ...styles.mediumText, marginTop: 20 }}
-        />
-      </NamespaceContext.Provider>
+    <Modal name="report-budget-summary">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Budget Summary"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <NamespaceContext.Provider value={sheetForMonth(month)}>
+            <Stack
+              spacing={2}
+              style={{
+                alignSelf: 'center',
+                backgroundColor: 'transparent',
+                borderRadius: 4,
+              }}
+            >
+              <IncomeTotal style={{ ...styles.mediumText }} />
+              <ExpenseTotal style={{ ...styles.mediumText }} />
+            </Stack>
+            <Saved
+              projected={month >= currentMonth}
+              style={{ ...styles.mediumText, marginTop: 20 }}
+            />
+          </NamespaceContext.Provider>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx
index 29a4ea856a14154e239ec43748d7d1fda7a68ccb..c35073f262d92b14ac3aa7d2a173ac3fac88fa16 100644
--- a/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx
@@ -9,19 +9,20 @@ import {
   DefaultCarryoverIndicator,
 } from '../budget/BalanceWithCarryover';
 import { BalanceMenu } from '../budget/rollover/BalanceMenu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 type RolloverBalanceMenuModalProps = ComponentPropsWithoutRef<
   typeof BalanceMenu
-> & {
-  modalProps: CommonModalProps;
-};
+>;
 
 export function RolloverBalanceMenuModal({
-  modalProps,
   categoryId,
   onCarryover,
   onTransfer,
@@ -41,57 +42,60 @@ export function RolloverBalanceMenuModal({
   }
 
   return (
-    <Modal
-      title={<ModalTitle title={category.name} shrinkOnOverflow />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          marginBottom: 20,
-        }}
-      >
-        <Text
-          style={{
-            fontSize: 17,
-            fontWeight: 400,
-          }}
-        >
-          Balance
-        </Text>
-        <BalanceWithCarryover
-          disabled
-          style={{
-            textAlign: 'center',
-            ...styles.veryLargeText,
-          }}
-          carryover={rolloverBudget.catCarryover(categoryId)}
-          balance={rolloverBudget.catBalance(categoryId)}
-          goal={rolloverBudget.catGoal(categoryId)}
-          budgeted={rolloverBudget.catBudgeted(categoryId)}
-          carryoverIndicator={({ style }) =>
-            DefaultCarryoverIndicator({
-              style: {
-                width: 15,
-                height: 15,
-                display: 'inline-flex',
-                position: 'relative',
-                ...style,
-              },
-            })
-          }
-        />
-      </View>
-      <BalanceMenu
-        categoryId={categoryId}
-        getItemStyle={() => defaultMenuItemStyle}
-        onCarryover={onCarryover}
-        onTransfer={onTransfer}
-        onCover={onCover}
-      />
+    <Modal name="rollover-balance-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title={category.name} shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 20,
+            }}
+          >
+            <Text
+              style={{
+                fontSize: 17,
+                fontWeight: 400,
+              }}
+            >
+              Balance
+            </Text>
+            <BalanceWithCarryover
+              disabled
+              style={{
+                textAlign: 'center',
+                ...styles.veryLargeText,
+              }}
+              carryover={rolloverBudget.catCarryover(categoryId)}
+              balance={rolloverBudget.catBalance(categoryId)}
+              goal={rolloverBudget.catGoal(categoryId)}
+              budgeted={rolloverBudget.catBudgeted(categoryId)}
+              carryoverIndicator={({ style }) =>
+                DefaultCarryoverIndicator({
+                  style: {
+                    width: 15,
+                    height: 15,
+                    display: 'inline-flex',
+                    position: 'relative',
+                    ...style,
+                  },
+                })
+              }
+            />
+          </View>
+          <BalanceMenu
+            categoryId={categoryId}
+            getItemStyle={() => defaultMenuItemStyle}
+            onCarryover={onCarryover}
+            onTransfer={onTransfer}
+            onCover={onCover}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx
index d54587acc991d6859d5f7e4846e9cea8ff19d379..5bd9c24407e9536d2b1bb8d759c319fc5069c908 100644
--- a/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx
@@ -10,23 +10,25 @@ import { amountToInteger, integerToAmount } from 'loot-core/shared/util';
 import { useCategory } from '../../hooks/useCategory';
 import { type CSSProperties, theme, styles } from '../../style';
 import { BudgetMenu } from '../budget/rollover/BudgetMenu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput';
-import { type CommonModalProps } from '../Modals';
 import { useSheetValue } from '../spreadsheet/useSheetValue';
 
 type RolloverBudgetMenuModalProps = ComponentPropsWithoutRef<
   typeof BudgetMenu
 > & {
-  modalProps: CommonModalProps;
   categoryId: string;
   onUpdateBudget: (amount: number) => void;
 };
 
 export function RolloverBudgetMenuModal({
-  modalProps,
   categoryId,
   onUpdateBudget,
   onCopyLastMonthAverage,
@@ -57,51 +59,54 @@ export function RolloverBudgetMenuModal({
   }
 
   return (
-    <Modal
-      title={<ModalTitle title={category.name} shrinkOnOverflow />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          marginBottom: 20,
-        }}
-      >
-        <Text
-          style={{
-            fontSize: 17,
-            fontWeight: 400,
-          }}
-        >
-          Budget
-        </Text>
-        <FocusableAmountInput
-          value={integerToAmount(budgeted || 0)}
-          focused={amountFocused}
-          onFocus={() => setAmountFocused(true)}
-          onBlur={() => setAmountFocused(false)}
-          onEnter={() => modalProps.onClose()}
-          zeroSign="+"
-          focusedStyle={{
-            width: 'auto',
-            padding: '5px',
-            paddingLeft: '20px',
-            paddingRight: '20px',
-            minWidth: '100%',
-          }}
-          textStyle={{ ...styles.veryLargeText, textAlign: 'center' }}
-          onUpdateAmount={_onUpdateBudget}
-        />
-      </View>
-      <BudgetMenu
-        getItemStyle={() => defaultMenuItemStyle}
-        onCopyLastMonthAverage={onCopyLastMonthAverage}
-        onSetMonthsAverage={onSetMonthsAverage}
-        onApplyBudgetTemplate={onApplyBudgetTemplate}
-      />
+    <Modal name="rollover-budget-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title={category.name} shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 20,
+            }}
+          >
+            <Text
+              style={{
+                fontSize: 17,
+                fontWeight: 400,
+              }}
+            >
+              Budget
+            </Text>
+            <FocusableAmountInput
+              value={integerToAmount(budgeted || 0)}
+              focused={amountFocused}
+              onFocus={() => setAmountFocused(true)}
+              onBlur={() => setAmountFocused(false)}
+              onEnter={close}
+              zeroSign="+"
+              focusedStyle={{
+                width: 'auto',
+                padding: '5px',
+                paddingLeft: '20px',
+                paddingRight: '20px',
+                minWidth: '100%',
+              }}
+              textStyle={{ ...styles.veryLargeText, textAlign: 'center' }}
+              onUpdateAmount={_onUpdateBudget}
+            />
+          </View>
+          <BudgetMenu
+            getItemStyle={() => defaultMenuItemStyle}
+            onCopyLastMonthAverage={onCopyLastMonthAverage}
+            onSetMonthsAverage={onSetMonthsAverage}
+            onApplyBudgetTemplate={onApplyBudgetTemplate}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx
index 70702422596d4b44fecb3fa54b27f5c515b64ec1..a34353e5dcb63f44cb6e22e0cb7ee67c0e5c457d 100644
--- a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx
@@ -9,30 +9,23 @@ import { SvgNotesPaper } from '../../icons/v2';
 import { type CSSProperties, styles, theme } from '../../style';
 import { BudgetMonthMenu } from '../budget/rollover/budgetsummary/BudgetMonthMenu';
 import { Button } from '../common/Button2';
-import { Modal, ModalTitle } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Notes } from '../Notes';
 
 type RolloverBudgetMonthMenuModalProps = {
-  modalProps: CommonModalProps;
   month: string;
   onBudgetAction: (month: string, action: string, arg?: unknown) => void;
   onEditNotes: (month: string) => void;
 };
 
 export function RolloverBudgetMonthMenuModal({
-  modalProps,
   month,
   onBudgetAction,
   onEditNotes,
 }: RolloverBudgetMonthMenuModalProps) {
   const originalNotes = useNotes(`budget-${month}`);
 
-  const onClose = () => {
-    modalProps.onClose();
-  };
-
   const _onEditNotes = () => {
     onEditNotes?.(month);
   };
@@ -60,124 +53,131 @@ export function RolloverBudgetMonthMenuModal({
 
   return (
     <Modal
-      title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-      onClose={onClose}
-      style={{
-        height: '50vh',
+      name="rollover-budget-month-menu"
+      containerProps={{
+        style: { height: '50vh' },
       }}
     >
-      <View
-        style={{
-          flex: 1,
-          flexDirection: 'column',
-        }}
-      >
-        <View
-          style={{
-            display: showMore ? 'none' : undefined,
-            overflowY: 'auto',
-            flex: 1,
-          }}
-        >
-          <Notes
-            notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
-            editable={false}
-            focused={false}
-            getStyle={() => ({
-              borderRadius: 6,
-              ...((!originalNotes || originalNotes.length === 0) && {
-                justifySelf: 'center',
-                alignSelf: 'center',
-                color: theme.pageTextSubdued,
-              }),
-            })}
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={monthUtils.format(month, 'MMMM ‘yy')}
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </View>
-        <View style={{ paddingTop: 10, gap: 5 }}>
           <View
             style={{
-              display: showMore ? 'none' : undefined,
-              flexDirection: 'row',
-              flexWrap: 'wrap',
-              justifyContent: 'space-between',
-              alignContent: 'space-between',
+              flex: 1,
+              flexDirection: 'column',
             }}
           >
-            <Button style={buttonStyle} onPress={_onEditNotes}>
-              <SvgNotesPaper
-                width={20}
-                height={20}
-                style={{ paddingRight: 5 }}
-              />
-              Edit notes
-            </Button>
-          </View>
-          <View>
-            <Button
-              variant="bare"
-              style={({ isPressed, isHovered }) => ({
-                ...buttonStyle,
-                ...(isPressed || isHovered
-                  ? { backgroundColor: 'transparent', color: buttonStyle.color }
-                  : {}),
-              })}
-              onPress={onShowMore}
+            <View
+              style={{
+                display: showMore ? 'none' : undefined,
+                overflowY: 'auto',
+                flex: 1,
+              }}
             >
-              {!showMore ? (
-                <SvgCheveronUp
-                  width={30}
-                  height={30}
-                  style={{ paddingRight: 5 }}
-                />
-              ) : (
-                <SvgCheveronDown
-                  width={30}
-                  height={30}
-                  style={{ paddingRight: 5 }}
-                />
-              )}
-              Actions
-            </Button>
+              <Notes
+                notes={originalNotes?.length > 0 ? originalNotes : 'No notes'}
+                editable={false}
+                focused={false}
+                getStyle={() => ({
+                  borderRadius: 6,
+                  ...((!originalNotes || originalNotes.length === 0) && {
+                    justifySelf: 'center',
+                    alignSelf: 'center',
+                    color: theme.pageTextSubdued,
+                  }),
+                })}
+              />
+            </View>
+            <View style={{ paddingTop: 10, gap: 5 }}>
+              <View
+                style={{
+                  display: showMore ? 'none' : undefined,
+                  flexDirection: 'row',
+                  flexWrap: 'wrap',
+                  justifyContent: 'space-between',
+                  alignContent: 'space-between',
+                }}
+              >
+                <Button style={buttonStyle} onPress={_onEditNotes}>
+                  <SvgNotesPaper
+                    width={20}
+                    height={20}
+                    style={{ paddingRight: 5 }}
+                  />
+                  Edit notes
+                </Button>
+              </View>
+              <View>
+                <Button
+                  variant="bare"
+                  style={({ isPressed, isHovered }) => ({
+                    ...buttonStyle,
+                    ...(isPressed || isHovered
+                      ? {
+                          backgroundColor: 'transparent',
+                          color: buttonStyle.color,
+                        }
+                      : {}),
+                  })}
+                  onPress={onShowMore}
+                >
+                  {!showMore ? (
+                    <SvgCheveronUp
+                      width={30}
+                      height={30}
+                      style={{ paddingRight: 5 }}
+                    />
+                  ) : (
+                    <SvgCheveronDown
+                      width={30}
+                      height={30}
+                      style={{ paddingRight: 5 }}
+                    />
+                  )}
+                  Actions
+                </Button>
+              </View>
+            </View>
+            {showMore && (
+              <BudgetMonthMenu
+                style={{ overflowY: 'auto', paddingTop: 10 }}
+                getItemStyle={() => defaultMenuItemStyle}
+                onCopyLastMonthBudget={() => {
+                  onBudgetAction(month, 'copy-last');
+                  close();
+                }}
+                onSetBudgetsToZero={() => {
+                  onBudgetAction(month, 'set-zero');
+                  close();
+                }}
+                onSetMonthsAverage={numberOfMonths => {
+                  onBudgetAction(month, `set-${numberOfMonths}-avg`);
+                  close();
+                }}
+                onCheckTemplates={() => {
+                  onBudgetAction(month, 'check-templates');
+                  close();
+                }}
+                onApplyBudgetTemplates={() => {
+                  onBudgetAction(month, 'apply-goal-template');
+                  close();
+                }}
+                onOverwriteWithBudgetTemplates={() => {
+                  onBudgetAction(month, 'overwrite-goal-template');
+                  close();
+                }}
+                onEndOfMonthCleanup={() => {
+                  onBudgetAction(month, 'cleanup-goal-template');
+                  close();
+                }}
+              />
+            )}
           </View>
-        </View>
-        {showMore && (
-          <BudgetMonthMenu
-            style={{ overflowY: 'auto', paddingTop: 10 }}
-            getItemStyle={() => defaultMenuItemStyle}
-            onCopyLastMonthBudget={() => {
-              onBudgetAction(month, 'copy-last');
-              onClose();
-            }}
-            onSetBudgetsToZero={() => {
-              onBudgetAction(month, 'set-zero');
-              onClose();
-            }}
-            onSetMonthsAverage={numberOfMonths => {
-              onBudgetAction(month, `set-${numberOfMonths}-avg`);
-              onClose();
-            }}
-            onCheckTemplates={() => {
-              onBudgetAction(month, 'check-templates');
-              onClose();
-            }}
-            onApplyBudgetTemplates={() => {
-              onBudgetAction(month, 'apply-goal-template');
-              onClose();
-            }}
-            onOverwriteWithBudgetTemplates={() => {
-              onBudgetAction(month, 'overwrite-goal-template');
-              onClose();
-            }}
-            onEndOfMonthCleanup={() => {
-              onBudgetAction(month, 'cleanup-goal-template');
-              onClose();
-            }}
-          />
-        )}
-      </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx
index a2bba5ee9fc1098ee62bc34307dcd18393a1e7c4..cbde09f1b4022daaada16d23ad8ec3042658664d 100644
--- a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx
+++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx
@@ -8,13 +8,11 @@ import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months';
 import { styles } from '../../style';
 import { ToBudgetAmount } from '../budget/rollover/budgetsummary/ToBudgetAmount';
 import { TotalsList } from '../budget/rollover/budgetsummary/TotalsList';
-import { Modal } from '../common/Modal';
-import { type CommonModalProps } from '../Modals';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { NamespaceContext } from '../spreadsheet/NamespaceContext';
 import { useSheetValue } from '../spreadsheet/useSheetValue';
 
 type RolloverBudgetSummaryModalProps = {
-  modalProps: CommonModalProps;
   onBudgetAction: (month: string, action: string, arg?: unknown) => void;
   month: string;
 };
@@ -22,7 +20,6 @@ type RolloverBudgetSummaryModalProps = {
 export function RolloverBudgetSummaryModal({
   month,
   onBudgetAction,
-  modalProps,
 }: RolloverBudgetSummaryModalProps) {
   const dispatch = useDispatch();
   const prevMonthName = format(prevMonth(month), 'MMM');
@@ -79,42 +76,52 @@ export function RolloverBudgetSummaryModal({
 
   const onResetHoldBuffer = () => {
     onBudgetAction(month, 'reset-hold');
-    modalProps.onClose();
   };
 
-  const onClick = () => {
+  const onClick = ({ close }: { close: () => void }) => {
     dispatch(
       pushModal('rollover-summary-to-budget-menu', {
         month,
         onTransfer: openTransferAvailableModal,
         onCover: openCoverOverbudgetedModal,
-        onResetHoldBuffer,
+        onResetHoldBuffer: () => {
+          onResetHoldBuffer();
+          close();
+        },
         onHoldBuffer,
       }),
     );
   };
 
   return (
-    <Modal title="Budget Summary" {...modalProps}>
-      <NamespaceContext.Provider value={sheetForMonth(month)}>
-        <TotalsList
-          prevMonthName={prevMonthName}
-          style={{
-            ...styles.mediumText,
-          }}
-        />
-        <ToBudgetAmount
-          prevMonthName={prevMonthName}
-          style={{
-            ...styles.mediumText,
-            marginTop: 15,
-          }}
-          amountStyle={{
-            ...styles.underlinedText,
-          }}
-          onClick={onClick}
-        />
-      </NamespaceContext.Provider>
+    <Modal name="rollover-budget-summary">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Budget Summary"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <NamespaceContext.Provider value={sheetForMonth(month)}>
+            <TotalsList
+              prevMonthName={prevMonthName}
+              style={{
+                ...styles.mediumText,
+              }}
+            />
+            <ToBudgetAmount
+              prevMonthName={prevMonthName}
+              style={{
+                ...styles.mediumText,
+                marginTop: 15,
+              }}
+              amountStyle={{
+                ...styles.underlinedText,
+              }}
+              onClick={() => onClick({ close })}
+            />
+          </NamespaceContext.Provider>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx
index c94e247be0e3423e28dbadc3be499911c00568fe..d21d5dbc29bf19ca40f75bda8f25080459c9d448 100644
--- a/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx
@@ -2,17 +2,13 @@ import React, { type ComponentPropsWithoutRef } from 'react';
 
 import { type CSSProperties, theme, styles } from '../../style';
 import { ToBudgetMenu } from '../budget/rollover/budgetsummary/ToBudgetMenu';
-import { Modal } from '../common/Modal';
-import { type CommonModalProps } from '../Modals';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 
 type RolloverToBudgetMenuModalProps = ComponentPropsWithoutRef<
   typeof ToBudgetMenu
-> & {
-  modalProps: CommonModalProps;
-};
+>;
 
 export function RolloverToBudgetMenuModal({
-  modalProps,
   onTransfer,
   onCover,
   onHoldBuffer,
@@ -26,14 +22,22 @@ export function RolloverToBudgetMenuModal({
   };
 
   return (
-    <Modal showHeader focusAfterClose={false} {...modalProps}>
-      <ToBudgetMenu
-        getItemStyle={() => defaultMenuItemStyle}
-        onTransfer={onTransfer}
-        onCover={onCover}
-        onHoldBuffer={onHoldBuffer}
-        onResetHoldBuffer={onResetHoldBuffer}
-      />
+    <Modal name="rollover-summary-to-budget-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            showLogo
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <ToBudgetMenu
+            getItemStyle={() => defaultMenuItemStyle}
+            onTransfer={onTransfer}
+            onCover={onCover}
+            onHoldBuffer={onHoldBuffer}
+            onResetHoldBuffer={onResetHoldBuffer}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx
index 46c31083f3798a415a197ecef7477e1af1c18877..0436554a7739fd8684cf9ae0e4be3827d8e7e2d8 100644
--- a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx
+++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx
@@ -6,17 +6,18 @@ import { type Query } from 'loot-core/shared/query';
 
 import { type CSSProperties, theme, styles } from '../../style';
 import { Menu } from '../common/Menu';
-import { Modal, ModalTitle } from '../common/Modal';
+import {
+  Modal,
+  ModalCloseButton,
+  ModalHeader,
+  ModalTitle,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
-type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps & {
-  modalProps: CommonModalProps;
-};
+type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps;
 
 export function ScheduledTransactionMenuModal({
-  modalProps,
   transactionId,
   onSkip,
   onPost,
@@ -41,30 +42,35 @@ export function ScheduledTransactionMenuModal({
   }
 
   return (
-    <Modal
-      title={<ModalTitle title={schedule.name || ''} shrinkOnOverflow />}
-      showHeader
-      focusAfterClose={false}
-      {...modalProps}
-    >
-      <View
-        style={{
-          justifyContent: 'center',
-          alignItems: 'center',
-          marginBottom: 20,
-        }}
-      >
-        <Text style={{ fontSize: 17, fontWeight: 400 }}>Scheduled date</Text>
-        <Text style={{ fontSize: 17, fontWeight: 700 }}>
-          {format(schedule.next_date, 'MMMM dd, yyyy')}
-        </Text>
-      </View>
-      <ScheduledTransactionMenu
-        transactionId={transactionId}
-        onPost={onPost}
-        onSkip={onSkip}
-        getItemStyle={() => defaultMenuItemStyle}
-      />
+    <Modal name="scheduled-transaction-menu">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={<ModalTitle title={schedule.name || ''} shrinkOnOverflow />}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              justifyContent: 'center',
+              alignItems: 'center',
+              marginBottom: 20,
+            }}
+          >
+            <Text style={{ fontSize: 17, fontWeight: 400 }}>
+              Scheduled date
+            </Text>
+            <Text style={{ fontSize: 17, fontWeight: 700 }}>
+              {format(schedule.next_date, 'MMMM dd, yyyy')}
+            </Text>
+          </View>
+          <ScheduledTransactionMenu
+            transactionId={transactionId}
+            onPost={onPost}
+            onSkip={onSkip}
+            getItemStyle={() => defaultMenuItemStyle}
+          />
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
index 4dcc8696cb82347f4e3c2fcc51fab315cf18bf85..f246eaaa3cd171480c2af7e635feec393311ff35 100644
--- a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
+++ b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx
@@ -1,10 +1,17 @@
 import React, { useState } from 'react';
+import { useDispatch } from 'react-redux';
+
+import {
+  linkAccount,
+  linkAccountSimpleFin,
+  unlinkAccount,
+} from 'loot-core/client/actions';
 
 import { useAccounts } from '../../hooks/useAccounts';
 import { theme } from '../../style';
 import { Autocomplete } from '../autocomplete/Autocomplete';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { PrivacyFilter } from '../PrivacyFilter';
@@ -17,13 +24,12 @@ const addOffBudgetAccountOption = {
 };
 
 export function SelectLinkedAccounts({
-  modalProps,
   requisitionId,
   externalAccounts,
-  actions,
   syncSource,
 }) {
   externalAccounts.sort((a, b) => a.name.localeCompare(b.name));
+  const dispatch = useDispatch();
   const localAccounts = useAccounts().filter(a => a.closed === 0);
   const [chosenAccounts, setChosenAccounts] = useState(() => {
     return Object.fromEntries(
@@ -41,7 +47,7 @@ export function SelectLinkedAccounts({
     localAccounts
       .filter(acc => acc.account_id)
       .filter(acc => !chosenLocalAccountIds.includes(acc.id))
-      .forEach(acc => actions.unlinkAccount(acc.id));
+      .forEach(acc => dispatch(unlinkAccount(acc.id)));
 
     // Link new accounts
     Object.entries(chosenAccounts).forEach(
@@ -59,29 +65,31 @@ export function SelectLinkedAccounts({
 
         // Finally link the matched account
         if (syncSource === 'simpleFin') {
-          actions.linkAccountSimpleFin(
-            externalAccount,
-            chosenLocalAccountId !== addOnBudgetAccountOption.id &&
-              chosenLocalAccountId !== addOffBudgetAccountOption.id
-              ? chosenLocalAccountId
-              : undefined,
-            offBudget,
+          dispatch(
+            linkAccountSimpleFin(
+              externalAccount,
+              chosenLocalAccountId !== addOnBudgetAccountOption.id &&
+                chosenLocalAccountId !== addOffBudgetAccountOption.id
+                ? chosenLocalAccountId
+                : undefined,
+              offBudget,
+            ),
           );
         } else {
-          actions.linkAccount(
-            requisitionId,
-            externalAccount,
-            chosenLocalAccountId !== addOnBudgetAccountOption.id &&
-              chosenLocalAccountId !== addOffBudgetAccountOption.id
-              ? chosenLocalAccountId
-              : undefined,
-            offBudget,
+          dispatch(
+            linkAccount(
+              requisitionId,
+              externalAccount,
+              chosenLocalAccountId !== addOnBudgetAccountOption.id &&
+                chosenLocalAccountId !== addOffBudgetAccountOption.id
+                ? chosenLocalAccountId
+                : undefined,
+              offBudget,
+            ),
           );
         }
       },
     );
-
-    actions.closeModal();
   }
 
   const unlinkedAccounts = localAccounts.filter(
@@ -103,9 +111,16 @@ export function SelectLinkedAccounts({
   }
 
   return (
-    <Modal title="Link Accounts" {...modalProps} style={{ width: 800 }}>
-      {() => (
+    <Modal
+      name="select-linked-accounts"
+      containerProps={{ style: { width: 800 } }}
+    >
+      {({ state: { close } }) => (
         <>
+          <ModalHeader
+            title="Link Accounts"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
           <Text style={{ marginBottom: 10 }}>
             We found the following accounts. Select which ones you want to add:
           </Text>
@@ -161,7 +176,10 @@ export function SelectLinkedAccounts({
           >
             <Button
               variant="primary"
-              onPress={onNext}
+              onPress={() => {
+                onNext();
+                close();
+              }}
               isDisabled={!Object.keys(chosenAccounts).length}
             >
               Link accounts
diff --git a/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx b/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx
index 37d0a2d788307344fb397ddc1045c71053aeb8a1..1392d38cc49c9a338e57d753808e6efdb24824c5 100644
--- a/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx
+++ b/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx
@@ -7,19 +7,21 @@ import { Error } from '../alerts';
 import { ButtonWithLoading } from '../common/Button2';
 import { Input } from '../common/Input';
 import { Link } from '../common/Link';
-import { Modal, ModalButtons } from '../common/Modal';
-import type { ModalProps } from '../common/Modal';
+import {
+  Modal,
+  ModalButtons,
+  ModalCloseButton,
+  ModalHeader,
+} from '../common/Modal2';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
 import { FormField, FormLabel } from '../forms';
 
 type SimpleFinInitialiseProps = {
-  modalProps?: Partial<ModalProps>;
   onSuccess: () => void;
 };
 
 export const SimpleFinInitialise = ({
-  modalProps,
   onSuccess,
 }: SimpleFinInitialiseProps) => {
   const [token, setToken] = useState('');
@@ -40,52 +42,62 @@ export const SimpleFinInitialise = ({
     });
 
     onSuccess();
-    modalProps.onClose();
     setIsLoading(false);
   };
 
   return (
-    <Modal title="Set-up SimpleFIN" size={{ width: 300 }} {...modalProps}>
-      <View style={{ display: 'flex', gap: 10 }}>
-        <Text>
-          In order to enable bank-sync via SimpleFIN (only for North American
-          banks) you will need to create a token. This can be done by creating
-          an account with{' '}
-          <Link
-            variant="external"
-            to="https://beta-bridge.simplefin.org/"
-            linkColor="purple"
-          >
-            SimpleFIN
-          </Link>
-          .
-        </Text>
-
-        <FormField>
-          <FormLabel title="Token:" htmlFor="token-field" />
-          <Input
-            id="token-field"
-            type="password"
-            value={token}
-            onChangeValue={value => {
-              setToken(value);
-              setIsValid(true);
-            }}
+    <Modal name="simplefin-init" containerProps={{ style: { width: 300 } }}>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Set-up SimpleFIN"
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-        </FormField>
+          <View style={{ display: 'flex', gap: 10 }}>
+            <Text>
+              In order to enable bank-sync via SimpleFIN (only for North
+              American banks) you will need to create a token. This can be done
+              by creating an account with{' '}
+              <Link
+                variant="external"
+                to="https://beta-bridge.simplefin.org/"
+                linkColor="purple"
+              >
+                SimpleFIN
+              </Link>
+              .
+            </Text>
+
+            <FormField>
+              <FormLabel title="Token:" htmlFor="token-field" />
+              <Input
+                id="token-field"
+                type="password"
+                value={token}
+                onChangeValue={value => {
+                  setToken(value);
+                  setIsValid(true);
+                }}
+              />
+            </FormField>
 
-        {!isValid && <Error>It is required to provide a token.</Error>}
-      </View>
+            {!isValid && <Error>It is required to provide a token.</Error>}
+          </View>
 
-      <ModalButtons>
-        <ButtonWithLoading
-          variant="primary"
-          isLoading={isLoading}
-          onPress={onSubmit}
-        >
-          Save and continue
-        </ButtonWithLoading>
-      </ModalButtons>
+          <ModalButtons>
+            <ButtonWithLoading
+              variant="primary"
+              isLoading={isLoading}
+              onPress={() => {
+                onSubmit();
+                close();
+              }}
+            >
+              Save and continue
+            </ButtonWithLoading>
+          </ModalButtons>
+        </>
+      )}
     </Modal>
   );
 };
diff --git a/packages/desktop-client/src/components/modals/SingleInputModal.tsx b/packages/desktop-client/src/components/modals/SingleInputModal.tsx
index 8ba91fcb7e294ec30e06b9a9933ceba6c26004e6..162a53f187dc2a0106bfea676651e48aaf644024 100644
--- a/packages/desktop-client/src/components/modals/SingleInputModal.tsx
+++ b/packages/desktop-client/src/components/modals/SingleInputModal.tsx
@@ -1,19 +1,23 @@
 // @ts-strict-ignore
-import React, { type ComponentProps, useState, type FormEvent } from 'react';
+import React, {
+  useState,
+  type ComponentType,
+  type ComponentPropsWithoutRef,
+  type FormEvent,
+} from 'react';
 import { Form } from 'react-aria-components';
 
 import { styles } from '../../style';
 import { Button } from '../common/Button2';
 import { FormError } from '../common/FormError';
 import { InitialFocus } from '../common/InitialFocus';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, type ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
 import { InputField } from '../mobile/MobileForms';
-import { type CommonModalProps } from '../Modals';
 
 type SingleInputModalProps = {
-  modalProps: Partial<CommonModalProps>;
-  title: ComponentProps<typeof Modal>['title'];
+  name: string;
+  Header: ComponentType<ComponentPropsWithoutRef<typeof ModalHeader>>;
   buttonText: string;
   onSubmit: (value: string) => void;
   onValidate?: (value: string) => string[];
@@ -21,8 +25,8 @@ type SingleInputModalProps = {
 };
 
 export function SingleInputModal({
-  modalProps,
-  title,
+  name,
+  Header,
   buttonText,
   onSubmit,
   onValidate,
@@ -41,52 +45,61 @@ export function SingleInputModal({
     }
 
     onSubmit?.(value);
-    modalProps.onClose();
   };
 
   return (
-    <Modal title={title} {...modalProps}>
-      <Form onSubmit={_onSubmit}>
-        <View>
-          <InitialFocus>
-            <InputField
-              placeholder={inputPlaceholder}
-              defaultValue={value}
-              onChangeValue={setValue}
-            />
-          </InitialFocus>
-          {errorMessage && (
-            <FormError
+    <Modal name={name}>
+      {({ state: { close } }) => (
+        <>
+          <Header rightContent={<ModalCloseButton onClick={close} />} />
+          <Form
+            onSubmit={e => {
+              _onSubmit(e);
+              close();
+            }}
+          >
+            <View>
+              <InitialFocus>
+                <InputField
+                  placeholder={inputPlaceholder}
+                  defaultValue={value}
+                  onChangeValue={setValue}
+                />
+              </InitialFocus>
+              {errorMessage && (
+                <FormError
+                  style={{
+                    paddingTop: 5,
+                    marginLeft: styles.mobileEditingPadding,
+                    marginRight: styles.mobileEditingPadding,
+                  }}
+                >
+                  * {errorMessage}
+                </FormError>
+              )}
+            </View>
+            <View
               style={{
-                paddingTop: 5,
-                marginLeft: styles.mobileEditingPadding,
-                marginRight: styles.mobileEditingPadding,
+                justifyContent: 'center',
+                alignItems: 'center',
+                paddingTop: 10,
               }}
             >
-              * {errorMessage}
-            </FormError>
-          )}
-        </View>
-        <View
-          style={{
-            justifyContent: 'center',
-            alignItems: 'center',
-            paddingTop: 10,
-          }}
-        >
-          <Button
-            type="submit"
-            variant="primary"
-            style={{
-              height: styles.mobileMinHeight,
-              marginLeft: styles.mobileEditingPadding,
-              marginRight: styles.mobileEditingPadding,
-            }}
-          >
-            {buttonText}
-          </Button>
-        </View>
-      </Form>
+              <Button
+                type="submit"
+                variant="primary"
+                style={{
+                  height: styles.mobileMinHeight,
+                  marginLeft: styles.mobileEditingPadding,
+                  marginRight: styles.mobileEditingPadding,
+                }}
+              >
+                {buttonText}
+              </Button>
+            </View>
+          </Form>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/modals/TransferModal.tsx b/packages/desktop-client/src/components/modals/TransferModal.tsx
index 0361c0685fc33f2b744b8f7d8ece3d8c75eda595..31b58ebd587a87c2a36f47277ff2e7abcba76e32 100644
--- a/packages/desktop-client/src/components/modals/TransferModal.tsx
+++ b/packages/desktop-client/src/components/modals/TransferModal.tsx
@@ -8,14 +8,12 @@ import { styles } from '../../style';
 import { addToBeBudgetedGroup } from '../budget/util';
 import { Button } from '../common/Button2';
 import { InitialFocus } from '../common/InitialFocus';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { View } from '../common/View';
 import { FieldLabel, TapField } from '../mobile/MobileForms';
-import { type CommonModalProps } from '../Modals';
 import { AmountInput } from '../util/AmountInput';
 
 type TransferModalProps = {
-  modalProps: CommonModalProps;
   title: string;
   month: string;
   amount: number;
@@ -24,7 +22,6 @@ type TransferModalProps = {
 };
 
 export function TransferModal({
-  modalProps,
   title,
   month,
   amount: initialAmount,
@@ -62,65 +59,74 @@ export function TransferModal({
     if (newAmount && categoryId) {
       onSubmit?.(newAmount, categoryId);
     }
-
-    modalProps.onClose();
   };
 
   const toCategory = categories.find(c => c.id === toCategoryId);
 
   return (
-    <Modal title={title} showHeader focusAfterClose={false} {...modalProps}>
-      <View>
-        <View>
-          <FieldLabel title="Transfer this amount:" />
-          <InitialFocus>
-            <AmountInput
-              value={initialAmount}
-              autoDecimals={true}
-              style={{
-                marginLeft: styles.mobileEditingPadding,
-                marginRight: styles.mobileEditingPadding,
-              }}
-              inputStyle={{
-                height: styles.mobileMinHeight,
-              }}
-              onUpdate={setAmount}
-              onEnter={() => {
-                if (!toCategoryId) {
-                  openCategoryModal();
-                }
-              }}
-            />
-          </InitialFocus>
-        </View>
+    <Modal name="transfer">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={title}
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View>
+            <View>
+              <FieldLabel title="Transfer this amount:" />
+              <InitialFocus>
+                <AmountInput
+                  value={initialAmount}
+                  autoDecimals={true}
+                  style={{
+                    marginLeft: styles.mobileEditingPadding,
+                    marginRight: styles.mobileEditingPadding,
+                  }}
+                  inputStyle={{
+                    height: styles.mobileMinHeight,
+                  }}
+                  onUpdate={setAmount}
+                  onEnter={() => {
+                    if (!toCategoryId) {
+                      openCategoryModal();
+                    }
+                  }}
+                />
+              </InitialFocus>
+            </View>
 
-        <FieldLabel title="To:" />
-        <TapField
-          tabIndex={0}
-          value={toCategory?.name}
-          onClick={openCategoryModal}
-        />
+            <FieldLabel title="To:" />
+            <TapField
+              tabIndex={0}
+              value={toCategory?.name}
+              onClick={openCategoryModal}
+            />
 
-        <View
-          style={{
-            justifyContent: 'center',
-            alignItems: 'center',
-            paddingTop: 10,
-          }}
-        >
-          <Button
-            variant="primary"
-            style={{
-              height: styles.mobileMinHeight,
-              marginLeft: styles.mobileEditingPadding,
-              marginRight: styles.mobileEditingPadding,
-            }}
-            onPress={() => _onSubmit(amount, toCategoryId)}
-          >
-            Transfer
-          </Button>
-        </View>
-      </View>
+            <View
+              style={{
+                justifyContent: 'center',
+                alignItems: 'center',
+                paddingTop: 10,
+              }}
+            >
+              <Button
+                variant="primary"
+                style={{
+                  height: styles.mobileMinHeight,
+                  marginLeft: styles.mobileEditingPadding,
+                  marginRight: styles.mobileEditingPadding,
+                }}
+                onPress={() => {
+                  _onSubmit(amount, toCategoryId);
+                  close();
+                }}
+              >
+                Transfer
+              </Button>
+            </View>
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
index e3238f741fd4c2daab5f49d283711b518fff6c21..745d2d6d5c085a9112f771998739badb25aae8bd 100644
--- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
+++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx
@@ -7,7 +7,6 @@ import { q } from 'loot-core/src/shared/query';
 import { getRecurringDescription } from 'loot-core/src/shared/schedules';
 import type { DiscoverScheduleEntity } from 'loot-core/src/types/models';
 
-import type { BoundActions } from '../../hooks/useActions';
 import { useDateFormat } from '../../hooks/useDateFormat';
 import {
   useSelected,
@@ -18,11 +17,10 @@ import {
 import { useSendPlatformRequest } from '../../hooks/useSendPlatformRequest';
 import { theme } from '../../style';
 import { ButtonWithLoading } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Stack } from '../common/Stack';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 import { Table, TableHeader, Row, Field, SelectCell } from '../table';
 import { DisplayId } from '../util/DisplayId';
 
@@ -124,13 +122,7 @@ function DiscoverSchedulesTable({
   );
 }
 
-export function DiscoverSchedules({
-  modalProps,
-  actions,
-}: {
-  modalProps: CommonModalProps;
-  actions: BoundActions;
-}) {
+export function DiscoverSchedules() {
   const { data, isLoading } = useSendPlatformRequest('schedule/discover');
 
   const schedules = data || [];
@@ -173,47 +165,57 @@ export function DiscoverSchedules({
     }
 
     setCreating(false);
-    actions.popModal();
   }
 
   return (
     <Modal
-      title="Found schedules"
-      size={{ width: 850, height: 650 }}
-      {...modalProps}
+      name="schedules-discover"
+      containerProps={{ style: { width: 850, height: 650 } }}
     >
-      <Paragraph>
-        We found some possible schedules in your current transactions. Select
-        the ones you want to create.
-      </Paragraph>
-      <Paragraph>
-        If you expected a schedule here and don’t see it, it might be because
-        the payees of the transactions don’t match. Make sure you rename payees
-        on all transactions for a schedule to be the same payee.
-      </Paragraph>
-
-      <SelectedProvider instance={selectedInst}>
-        <DiscoverSchedulesTable loading={isLoading} schedules={schedules} />
-      </SelectedProvider>
-
-      <Stack
-        direction="row"
-        align="center"
-        justify="flex-end"
-        style={{
-          paddingTop: 20,
-          paddingBottom: 0,
-        }}
-      >
-        <ButtonWithLoading
-          variant="primary"
-          isLoading={creating}
-          isDisabled={selectedInst.items.size === 0}
-          onPress={onCreate}
-        >
-          Create schedules
-        </ButtonWithLoading>
-      </Stack>
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Found Schedules"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <Paragraph>
+            We found some possible schedules in your current transactions.
+            Select the ones you want to create.
+          </Paragraph>
+          <Paragraph>
+            If you expected a schedule here and don’t see it, it might be
+            because the payees of the transactions don’t match. Make sure you
+            rename payees on all transactions for a schedule to be the same
+            payee.
+          </Paragraph>
+
+          <SelectedProvider instance={selectedInst}>
+            <DiscoverSchedulesTable loading={isLoading} schedules={schedules} />
+          </SelectedProvider>
+
+          <Stack
+            direction="row"
+            align="center"
+            justify="flex-end"
+            style={{
+              paddingTop: 20,
+              paddingBottom: 0,
+            }}
+          >
+            <ButtonWithLoading
+              variant="primary"
+              isLoading={creating}
+              isDisabled={selectedInst.items.size === 0}
+              onPress={() => {
+                onCreate();
+                close();
+              }}
+            >
+              Create schedules
+            </ButtonWithLoading>
+          </Stack>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
index 429c5119d2143ed4f4f44445182538c3e76c1932..b19c0dc9e503ed648ce4b1fb19f06b28f4d1e364 100644
--- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
+++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx
@@ -1,78 +1,96 @@
 import React from 'react';
+import { useDispatch } from 'react-redux';
 import { useLocation } from 'react-router-dom';
 
+import { popModal } from 'loot-core/client/actions';
 import { send } from 'loot-core/src/platform/client/fetch';
 
 import { theme } from '../../style';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Paragraph } from '../common/Paragraph';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
 import { DisplayId } from '../util/DisplayId';
 
-export function PostsOfflineNotification({ modalProps, actions }) {
+export function PostsOfflineNotification() {
   const location = useLocation();
+  const dispatch = useDispatch();
 
   const payees = (location.state && location.state.payees) || [];
   const plural = payees.length > 1;
 
   async function onPost() {
     await send('schedule/force-run-service');
-    actions.popModal();
+    dispatch(popModal());
   }
 
   return (
-    <Modal title="Post transactions?" size="small" {...modalProps}>
-      <Paragraph>
-        {payees.length > 0 ? (
-          <Text>
-            The {plural ? 'payees ' : 'payee '}
-            {payees.map((id, idx) => (
-              <Text key={id}>
-                <Text style={{ color: theme.pageTextPositive }}>
-                  <DisplayId id={id} type="payees" />
-                </Text>
-                {idx === payees.length - 1
-                  ? ' '
-                  : idx === payees.length - 2
-                    ? ', and '
-                    : ', '}
+    <Modal name="schedule-posts-offline-notification">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Post transactions?"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <Paragraph>
+            {payees.length > 0 ? (
+              <Text>
+                The {plural ? 'payees ' : 'payee '}
+                {payees.map((id, idx) => (
+                  <Text key={id}>
+                    <Text style={{ color: theme.pageTextPositive }}>
+                      <DisplayId id={id} type="payees" />
+                    </Text>
+                    {idx === payees.length - 1
+                      ? ' '
+                      : idx === payees.length - 2
+                        ? ', and '
+                        : ', '}
+                  </Text>
+                ))}
               </Text>
-            ))}
-          </Text>
-        ) : (
-          <Text>There {plural ? 'are payees ' : 'is a payee '} that </Text>
-        )}
+            ) : (
+              <Text>There {plural ? 'are payees ' : 'is a payee '} that </Text>
+            )}
 
-        <Text>
-          {plural ? 'have ' : 'has '} schedules that are due today. Usually we
-          automatically post transactions for these, but you are offline or
-          syncing failed. In order to avoid duplicate transactions, we let you
-          choose whether or not to create transactions for these schedules.
-        </Text>
-      </Paragraph>
-      <Paragraph>
-        Be aware that other devices may have already created these transactions.
-        If you have multiple devices, make sure you only do this on one device
-        or you will have duplicate transactions.
-      </Paragraph>
-      <Paragraph>
-        You can always manually post a transaction later for a due schedule by
-        selecting the schedule and clicking “Post transaction” in the action
-        menu.
-      </Paragraph>
-      <Stack
-        direction="row"
-        justify="flex-end"
-        style={{ marginTop: 20 }}
-        spacing={2}
-      >
-        <Button onPress={actions.popModal}>Decide later</Button>
-        <Button variant="primary" onPress={onPost}>
-          Post transactions
-        </Button>
-      </Stack>
+            <Text>
+              {plural ? 'have ' : 'has '} schedules that are due today. Usually
+              we automatically post transactions for these, but you are offline
+              or syncing failed. In order to avoid duplicate transactions, we
+              let you choose whether or not to create transactions for these
+              schedules.
+            </Text>
+          </Paragraph>
+          <Paragraph>
+            Be aware that other devices may have already created these
+            transactions. If you have multiple devices, make sure you only do
+            this on one device or you will have duplicate transactions.
+          </Paragraph>
+          <Paragraph>
+            You can always manually post a transaction later for a due schedule
+            by selecting the schedule and clicking “Post transaction” in the
+            action menu.
+          </Paragraph>
+          <Stack
+            direction="row"
+            justify="flex-end"
+            style={{ marginTop: 20 }}
+            spacing={2}
+          >
+            <Button onPress={close}>Decide later</Button>
+            <Button
+              variant="primary"
+              onPress={() => {
+                onPost();
+                close();
+              }}
+            >
+              Post transactions
+            </Button>
+          </Stack>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
index 12d4b9ca4c295f7c44d874fe6597637e0f8e0bfd..5c89c4fc27519d0c6f16501248b949cdfc1711c3 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx
@@ -16,7 +16,7 @@ import { theme } from '../../style';
 import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete';
 import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Stack } from '../common/Stack';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
@@ -69,7 +69,7 @@ function updateScheduleConditions(schedule, fields) {
   };
 }
 
-export function ScheduleDetails({ modalProps, actions, id, transaction }) {
+export function ScheduleDetails({ id, transaction }) {
   const adding = id == null;
   const fromTrans = transaction != null;
   const payees = getPayeesById(usePayees());
@@ -400,7 +400,6 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) {
       if (adding) {
         await onLinkTransactions([...selectedInst.items], res.data);
       }
-      actions.popModal();
     }
   }
 
@@ -448,342 +447,356 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) {
   // This is derived from the date
   const repeats = state.fields.date ? !!state.fields.date.frequency : false;
   return (
-    <Modal
-      title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
-      size="medium"
-      {...modalProps}
-    >
-      <Stack direction="row" style={{ marginTop: 10 }}>
-        <FormField style={{ flex: 1 }}>
-          <FormLabel title="Schedule Name" htmlFor="name-field" />
-          <GenericInput
-            field="string"
-            type="string"
-            value={state.fields.name}
-            multi={false}
-            onChange={e => {
-              dispatch({ type: 'set-field', field: 'name', value: e });
-            }}
-          />
-        </FormField>
-      </Stack>
-      <Stack direction="row" style={{ marginTop: 20 }}>
-        <FormField style={{ flex: 1 }}>
-          <FormLabel title="Payee" id="payee-label" htmlFor="payee-field" />
-          <PayeeAutocomplete
-            value={state.fields.payee}
-            labelProps={{ id: 'payee-label' }}
-            inputProps={{ id: 'payee-field', placeholder: '(none)' }}
-            onSelect={id =>
-              dispatch({ type: 'set-field', field: 'payee', value: id })
-            }
-          />
-        </FormField>
-
-        <FormField style={{ flex: 1 }}>
-          <FormLabel
-            title="Account"
-            id="account-label"
-            htmlFor="account-field"
+    <Modal name="schedule-edit">
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title={payee ? `Schedule: ${payee.name}` : 'Schedule'}
+            rightContent={<ModalCloseButton onClick={close} />}
           />
-          <AccountAutocomplete
-            includeClosedAccounts={false}
-            value={state.fields.account}
-            labelProps={{ id: 'account-label' }}
-            inputProps={{ id: 'account-field', placeholder: '(none)' }}
-            onSelect={id =>
-              dispatch({ type: 'set-field', field: 'account', value: id })
-            }
-          />
-        </FormField>
-
-        <FormField style={{ flex: 1 }}>
-          <Stack direction="row" align="center" style={{ marginBottom: 3 }}>
-            <FormLabel
-              title="Amount"
-              htmlFor="amount-field"
-              style={{ margin: 0, flex: 1 }}
-            />
-            <OpSelect
-              ops={['isapprox', 'is', 'isbetween']}
-              value={state.fields.amountOp}
-              formatOp={op => {
-                switch (op) {
-                  case 'is':
-                    return 'is exactly';
-                  case 'isapprox':
-                    return 'is approximately';
-                  case 'isbetween':
-                    return 'is between';
-                  default:
-                    throw new Error('Invalid op for select: ' + op);
-                }
-              }}
-              style={{
-                padding: '0 10px',
-                color: theme.pageTextLight,
-                fontSize: 12,
-              }}
-              onChange={(_, op) =>
-                dispatch({ type: 'set-field', field: 'amountOp', value: op })
-              }
-            />
+          <Stack direction="row" style={{ marginTop: 10 }}>
+            <FormField style={{ flex: 1 }}>
+              <FormLabel title="Schedule Name" htmlFor="name-field" />
+              <GenericInput
+                field="string"
+                type="string"
+                value={state.fields.name}
+                multi={false}
+                onChange={e => {
+                  dispatch({ type: 'set-field', field: 'name', value: e });
+                }}
+              />
+            </FormField>
           </Stack>
-          {state.fields.amountOp === 'isbetween' ? (
-            <BetweenAmountInput
-              defaultValue={state.fields.amount}
-              onChange={value =>
-                dispatch({
-                  type: 'set-field',
-                  field: 'amount',
-                  value,
-                })
-              }
-            />
-          ) : (
-            <AmountInput
-              id="amount-field"
-              value={state.fields.amount}
-              onUpdate={value =>
-                dispatch({
-                  type: 'set-field',
-                  field: 'amount',
-                  value,
-                })
-              }
-            />
-          )}
-        </FormField>
-      </Stack>
-
-      <View style={{ marginTop: 20 }}>
-        <FormLabel title="Date" />
-      </View>
-
-      <Stack direction="row" align="flex-start" justify="space-between">
-        <View style={{ width: '13.44rem' }}>
-          {repeats ? (
-            <RecurringSchedulePicker
-              value={state.fields.date}
-              onChange={value =>
-                dispatch({ type: 'set-field', field: 'date', value })
-              }
-            />
-          ) : (
-            <DateSelect
-              value={state.fields.date}
-              onSelect={date =>
-                dispatch({ type: 'set-field', field: 'date', value: date })
-              }
-              dateFormat={dateFormat}
-            />
-          )}
-
-          {state.upcomingDates && (
-            <View style={{ fontSize: 13, marginTop: 20 }}>
-              <Text style={{ color: theme.pageTextLight, fontWeight: 600 }}>
-                Upcoming dates
-              </Text>
-              <Stack
-                direction="column"
-                spacing={1}
-                style={{ marginTop: 10, color: theme.pageTextLight }}
-              >
-                {state.upcomingDates.map(date => (
-                  <View key={date}>
-                    {monthUtils.format(date, `${dateFormat} EEEE`)}
-                  </View>
-                ))}
-              </Stack>
-            </View>
-          )}
-        </View>
-
-        <View
-          style={{
-            marginTop: 5,
-            flexDirection: 'row',
-            alignItems: 'center',
-            userSelect: 'none',
-          }}
-        >
-          <Checkbox
-            id="form_repeats"
-            checked={repeats}
-            onChange={e => {
-              dispatch({ type: 'set-repeats', repeats: e.target.checked });
-            }}
-          />
-          <label htmlFor="form_repeats" style={{ userSelect: 'none' }}>
-            Repeats
-          </label>
-        </View>
-
-        <Stack align="flex-end">
-          <View
-            style={{
-              marginTop: 5,
-              flexDirection: 'row',
-              alignItems: 'center',
-              userSelect: 'none',
-              justifyContent: 'flex-end',
-            }}
-          >
-            <Checkbox
-              id="form_posts_transaction"
-              checked={state.fields.posts_transaction}
-              onChange={e => {
-                dispatch({
-                  type: 'set-field',
-                  field: 'posts_transaction',
-                  value: e.target.checked,
-                });
-              }}
-            />
-            <label
-              htmlFor="form_posts_transaction"
-              style={{ userSelect: 'none' }}
-            >
-              Automatically add transaction
-            </label>
-          </View>
+          <Stack direction="row" style={{ marginTop: 20 }}>
+            <FormField style={{ flex: 1 }}>
+              <FormLabel title="Payee" id="payee-label" htmlFor="payee-field" />
+              <PayeeAutocomplete
+                value={state.fields.payee}
+                labelProps={{ id: 'payee-label' }}
+                inputProps={{ id: 'payee-field', placeholder: '(none)' }}
+                onSelect={id =>
+                  dispatch({ type: 'set-field', field: 'payee', value: id })
+                }
+              />
+            </FormField>
 
-          <Text
-            style={{
-              width: 350,
-              textAlign: 'right',
-              color: theme.pageTextLight,
-              marginTop: 10,
-              fontSize: 13,
-              lineHeight: '1.4em',
-            }}
-          >
-            If checked, the schedule will automatically create transactions for
-            you in the specified account
-          </Text>
-
-          {!adding && state.schedule.rule && (
-            <Stack direction="row" align="center" style={{ marginTop: 20 }}>
-              {state.isCustom && (
-                <Text
+            <FormField style={{ flex: 1 }}>
+              <FormLabel
+                title="Account"
+                id="account-label"
+                htmlFor="account-field"
+              />
+              <AccountAutocomplete
+                includeClosedAccounts={false}
+                value={state.fields.account}
+                labelProps={{ id: 'account-label' }}
+                inputProps={{ id: 'account-field', placeholder: '(none)' }}
+                onSelect={id =>
+                  dispatch({ type: 'set-field', field: 'account', value: id })
+                }
+              />
+            </FormField>
+
+            <FormField style={{ flex: 1 }}>
+              <Stack direction="row" align="center" style={{ marginBottom: 3 }}>
+                <FormLabel
+                  title="Amount"
+                  htmlFor="amount-field"
+                  style={{ margin: 0, flex: 1 }}
+                />
+                <OpSelect
+                  ops={['isapprox', 'is', 'isbetween']}
+                  value={state.fields.amountOp}
+                  formatOp={op => {
+                    switch (op) {
+                      case 'is':
+                        return 'is exactly';
+                      case 'isapprox':
+                        return 'is approximately';
+                      case 'isbetween':
+                        return 'is between';
+                      default:
+                        throw new Error('Invalid op for select: ' + op);
+                    }
+                  }}
                   style={{
+                    padding: '0 10px',
                     color: theme.pageTextLight,
-                    fontSize: 13,
-                    textAlign: 'right',
-                    width: 350,
+                    fontSize: 12,
                   }}
-                >
-                  This schedule has custom conditions and actions
-                </Text>
+                  onChange={(_, op) =>
+                    dispatch({
+                      type: 'set-field',
+                      field: 'amountOp',
+                      value: op,
+                    })
+                  }
+                />
+              </Stack>
+              {state.fields.amountOp === 'isbetween' ? (
+                <BetweenAmountInput
+                  defaultValue={state.fields.amount}
+                  onChange={value =>
+                    dispatch({
+                      type: 'set-field',
+                      field: 'amount',
+                      value,
+                    })
+                  }
+                />
+              ) : (
+                <AmountInput
+                  id="amount-field"
+                  value={state.fields.amount}
+                  onUpdate={value =>
+                    dispatch({
+                      type: 'set-field',
+                      field: 'amount',
+                      value,
+                    })
+                  }
+                />
+              )}
+            </FormField>
+          </Stack>
+
+          <View style={{ marginTop: 20 }}>
+            <FormLabel title="Date" />
+          </View>
+
+          <Stack direction="row" align="flex-start" justify="space-between">
+            <View style={{ width: '13.44rem' }}>
+              {repeats ? (
+                <RecurringSchedulePicker
+                  value={state.fields.date}
+                  onChange={value =>
+                    dispatch({ type: 'set-field', field: 'date', value })
+                  }
+                />
+              ) : (
+                <DateSelect
+                  value={state.fields.date}
+                  onSelect={date =>
+                    dispatch({ type: 'set-field', field: 'date', value: date })
+                  }
+                  dateFormat={dateFormat}
+                />
+              )}
+
+              {state.upcomingDates && (
+                <View style={{ fontSize: 13, marginTop: 20 }}>
+                  <Text style={{ color: theme.pageTextLight, fontWeight: 600 }}>
+                    Upcoming dates
+                  </Text>
+                  <Stack
+                    direction="column"
+                    spacing={1}
+                    style={{ marginTop: 10, color: theme.pageTextLight }}
+                  >
+                    {state.upcomingDates.map(date => (
+                      <View key={date}>
+                        {monthUtils.format(date, `${dateFormat} EEEE`)}
+                      </View>
+                    ))}
+                  </Stack>
+                </View>
               )}
-              <Button onPress={() => onEditRule()} isDisabled={adding}>
-                Edit as rule
-              </Button>
-            </Stack>
-          )}
-        </Stack>
-      </Stack>
-
-      <View style={{ marginTop: 30, flex: 1 }}>
-        <SelectedProvider instance={selectedInst}>
-          {adding ? (
-            <View style={{ flexDirection: 'row', padding: '5px 0' }}>
-              <Text style={{ color: theme.pageTextLight }}>
-                These transactions match this schedule:
-              </Text>
-              <View style={{ flex: 1 }} />
-              <Text style={{ color: theme.pageTextLight }}>
-                Select transactions to link on save
-              </Text>
             </View>
-          ) : (
-            <View style={{ flexDirection: 'row', alignItems: 'center' }}>
-              <Button
-                variant="bare"
+
+            <View
+              style={{
+                marginTop: 5,
+                flexDirection: 'row',
+                alignItems: 'center',
+                userSelect: 'none',
+              }}
+            >
+              <Checkbox
+                id="form_repeats"
+                checked={repeats}
+                onChange={e => {
+                  dispatch({ type: 'set-repeats', repeats: e.target.checked });
+                }}
+              />
+              <label htmlFor="form_repeats" style={{ userSelect: 'none' }}>
+                Repeats
+              </label>
+            </View>
+
+            <Stack align="flex-end">
+              <View
                 style={{
-                  color:
-                    state.transactionsMode === 'linked'
-                      ? theme.pageTextLink
-                      : theme.pageTextSubdued,
-                  marginRight: 10,
-                  fontSize: 14,
+                  marginTop: 5,
+                  flexDirection: 'row',
+                  alignItems: 'center',
+                  userSelect: 'none',
+                  justifyContent: 'flex-end',
                 }}
-                onPress={() => onSwitchTransactions('linked')}
               >
-                Linked transactions
-              </Button>{' '}
-              <Button
-                variant="bare"
+                <Checkbox
+                  id="form_posts_transaction"
+                  checked={state.fields.posts_transaction}
+                  onChange={e => {
+                    dispatch({
+                      type: 'set-field',
+                      field: 'posts_transaction',
+                      value: e.target.checked,
+                    });
+                  }}
+                />
+                <label
+                  htmlFor="form_posts_transaction"
+                  style={{ userSelect: 'none' }}
+                >
+                  Automatically add transaction
+                </label>
+              </View>
+
+              <Text
                 style={{
-                  color:
-                    state.transactionsMode === 'matched'
-                      ? theme.pageTextLink
-                      : theme.pageTextSubdued,
-                  fontSize: 14,
+                  width: 350,
+                  textAlign: 'right',
+                  color: theme.pageTextLight,
+                  marginTop: 10,
+                  fontSize: 13,
+                  lineHeight: '1.4em',
                 }}
-                onPress={() => onSwitchTransactions('matched')}
               >
-                Find matching transactions
-              </Button>
-              <View style={{ flex: 1 }} />
-              <SelectedItemsButton
-                name="transactions"
-                items={
-                  state.transactionsMode === 'linked'
-                    ? [{ name: 'unlink', text: 'Unlink from schedule' }]
-                    : [{ name: 'link', text: 'Link to schedule' }]
+                If checked, the schedule will automatically create transactions
+                for you in the specified account
+              </Text>
+
+              {!adding && state.schedule.rule && (
+                <Stack direction="row" align="center" style={{ marginTop: 20 }}>
+                  {state.isCustom && (
+                    <Text
+                      style={{
+                        color: theme.pageTextLight,
+                        fontSize: 13,
+                        textAlign: 'right',
+                        width: 350,
+                      }}
+                    >
+                      This schedule has custom conditions and actions
+                    </Text>
+                  )}
+                  <Button onPress={() => onEditRule()} isDisabled={adding}>
+                    Edit as rule
+                  </Button>
+                </Stack>
+              )}
+            </Stack>
+          </Stack>
+
+          <View style={{ marginTop: 30, flex: 1 }}>
+            <SelectedProvider instance={selectedInst}>
+              {adding ? (
+                <View style={{ flexDirection: 'row', padding: '5px 0' }}>
+                  <Text style={{ color: theme.pageTextLight }}>
+                    These transactions match this schedule:
+                  </Text>
+                  <View style={{ flex: 1 }} />
+                  <Text style={{ color: theme.pageTextLight }}>
+                    Select transactions to link on save
+                  </Text>
+                </View>
+              ) : (
+                <View style={{ flexDirection: 'row', alignItems: 'center' }}>
+                  <Button
+                    variant="bare"
+                    style={{
+                      color:
+                        state.transactionsMode === 'linked'
+                          ? theme.pageTextLink
+                          : theme.pageTextSubdued,
+                      marginRight: 10,
+                      fontSize: 14,
+                    }}
+                    onPress={() => onSwitchTransactions('linked')}
+                  >
+                    Linked transactions
+                  </Button>{' '}
+                  <Button
+                    variant="bare"
+                    style={{
+                      color:
+                        state.transactionsMode === 'matched'
+                          ? theme.pageTextLink
+                          : theme.pageTextSubdued,
+                      fontSize: 14,
+                    }}
+                    onPress={() => onSwitchTransactions('matched')}
+                  >
+                    Find matching transactions
+                  </Button>
+                  <View style={{ flex: 1 }} />
+                  <SelectedItemsButton
+                    name="transactions"
+                    items={
+                      state.transactionsMode === 'linked'
+                        ? [{ name: 'unlink', text: 'Unlink from schedule' }]
+                        : [{ name: 'link', text: 'Link to schedule' }]
+                    }
+                    onSelect={(name, ids) => {
+                      switch (name) {
+                        case 'link':
+                          onLinkTransactions(ids);
+                          break;
+                        case 'unlink':
+                          onUnlinkTransactions(ids);
+                          break;
+                        default:
+                      }
+                    }}
+                  />
+                </View>
+              )}
+
+              <SimpleTransactionsTable
+                renderEmpty={
+                  <NoTransactionsMessage
+                    error={state.error}
+                    transactionsMode={state.transactionsMode}
+                  />
                 }
-                onSelect={(name, ids) => {
-                  switch (name) {
-                    case 'link':
-                      onLinkTransactions(ids);
-                      break;
-                    case 'unlink':
-                      onUnlinkTransactions(ids);
-                      break;
-                    default:
-                  }
+                transactions={state.transactions}
+                fields={['date', 'payee', 'amount']}
+                style={{
+                  border: '1px solid ' + theme.tableBorder,
+                  borderRadius: 4,
+                  overflow: 'hidden',
+                  marginTop: 5,
+                  maxHeight: 200,
                 }}
               />
-            </View>
-          )}
+            </SelectedProvider>
+          </View>
 
-          <SimpleTransactionsTable
-            renderEmpty={
-              <NoTransactionsMessage
-                error={state.error}
-                transactionsMode={state.transactionsMode}
-              />
-            }
-            transactions={state.transactions}
-            fields={['date', 'payee', 'amount']}
-            style={{
-              border: '1px solid ' + theme.tableBorder,
-              borderRadius: 4,
-              overflow: 'hidden',
-              marginTop: 5,
-              maxHeight: 200,
-            }}
-          />
-        </SelectedProvider>
-      </View>
-
-      <Stack
-        direction="row"
-        justify="flex-end"
-        align="center"
-        style={{ marginTop: 20 }}
-      >
-        {state.error && (
-          <Text style={{ color: theme.errorText }}>{state.error}</Text>
-        )}
-        <Button style={{ marginRight: 10 }} onPress={actions.popModal}>
-          Cancel
-        </Button>
-        <Button variant="primary" onPress={onSave}>
-          {adding ? 'Add' : 'Save'}
-        </Button>
-      </Stack>
+          <Stack
+            direction="row"
+            justify="flex-end"
+            align="center"
+            style={{ marginTop: 20 }}
+          >
+            {state.error && (
+              <Text style={{ color: theme.errorText }}>{state.error}</Text>
+            )}
+            <Button style={{ marginRight: 10 }} onPress={close}>
+              Cancel
+            </Button>
+            <Button
+              variant="primary"
+              onPress={() => {
+                onSave();
+                close();
+              }}
+            >
+              {adding ? 'Add' : 'Save'}
+            </Button>
+          </Stack>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
index 7f989a784793b8d21339b9dfe1f1d2dad8b55349..666a040c4fe76a1d76852edf4656ab48d5bd3370 100644
--- a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
+++ b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx
@@ -8,25 +8,19 @@ import { send } from 'loot-core/src/platform/client/fetch';
 import { type Query } from 'loot-core/src/shared/query';
 import { type TransactionEntity } from 'loot-core/src/types/models';
 
-import { type BoundActions } from '../../hooks/useActions';
 import { SvgAdd } from '../../icons/v0';
 import { Button } from '../common/Button2';
-import { Modal } from '../common/Modal';
+import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2';
 import { Search } from '../common/Search';
 import { Text } from '../common/Text';
 import { View } from '../common/View';
-import { type CommonModalProps } from '../Modals';
 
 import { ROW_HEIGHT, SchedulesTable } from './SchedulesTable';
 
 export function ScheduleLink({
-  modalProps,
-  actions,
   transactionIds: ids,
   getTransaction,
 }: {
-  actions: BoundActions;
-  modalProps?: CommonModalProps;
   transactionIds: string[];
   getTransaction: (transactionId: string) => TransactionEntity;
 }) {
@@ -50,11 +44,9 @@ export function ScheduleLink({
         updated: ids.map(id => ({ id, schedule: scheduleId })),
       });
     }
-    actions.popModal();
   }
 
   async function onCreate() {
-    actions.popModal();
     dispatch(
       pushModal('schedule-edit', {
         id: null,
@@ -64,62 +56,83 @@ export function ScheduleLink({
   }
 
   return (
-    <Modal title="Link Schedule" size={{ width: 800 }} {...modalProps}>
-      <View
-        style={{
-          flexDirection: 'row',
-          gap: 4,
-          marginBottom: 20,
-          alignItems: 'center',
-        }}
-      >
-        <Text>
-          Choose the schedule{' '}
-          {ids?.length > 1
-            ? `these ${ids.length} transactions belong`
-            : `this transaction belongs`}{' '}
-          to:
-        </Text>
-        <Search
-          inputRef={searchInput}
-          isInModal
-          width={300}
-          placeholder="Filter schedules…"
-          value={filter}
-          onChange={setFilter}
-        />
-        {ids.length === 1 && (
-          <Button
-            variant="primary"
-            style={{ marginLeft: 15, padding: '4px 10px' }}
-            onPress={onCreate}
+    <Modal
+      name="schedule-link"
+      containerProps={{
+        style: {
+          width: 800,
+        },
+      }}
+    >
+      {({ state: { close } }) => (
+        <>
+          <ModalHeader
+            title="Link Schedule"
+            rightContent={<ModalCloseButton onClick={close} />}
+          />
+          <View
+            style={{
+              flexDirection: 'row',
+              gap: 4,
+              marginBottom: 20,
+              alignItems: 'center',
+            }}
           >
-            <SvgAdd style={{ width: '20', padding: '3' }} />
-            Create New
-          </Button>
-        )}
-      </View>
+            <Text>
+              Choose the schedule{' '}
+              {ids?.length > 1
+                ? `these ${ids.length} transactions belong`
+                : `this transaction belongs`}{' '}
+              to:
+            </Text>
+            <Search
+              inputRef={searchInput}
+              isInModal
+              width={300}
+              placeholder="Filter schedules…"
+              value={filter}
+              onChange={setFilter}
+            />
+            {ids.length === 1 && (
+              <Button
+                variant="primary"
+                style={{ marginLeft: 15, padding: '4px 10px' }}
+                onPress={() => {
+                  close();
+                  onCreate();
+                }}
+              >
+                <SvgAdd style={{ width: '20', padding: '3' }} />
+                Create New
+              </Button>
+            )}
+          </View>
 
-      <View
-        style={{
-          flex: `1 1 ${
-            (ROW_HEIGHT - 1) * (Math.max(schedules.length, 1) + 1)
-          }px`,
-          marginTop: 15,
-          maxHeight: '50vh',
-        }}
-      >
-        <SchedulesTable
-          allowCompleted={false}
-          filter={filter}
-          minimal={true}
-          onAction={() => {}}
-          onSelect={onSelect}
-          schedules={schedules}
-          statuses={statuses}
-          style={null}
-        />
-      </View>
+          <View
+            style={{
+              flex: `1 1 ${
+                (ROW_HEIGHT - 1) * (Math.max(schedules.length, 1) + 1)
+              }px`,
+              marginTop: 15,
+              maxHeight: '50vh',
+            }}
+          >
+            <SchedulesTable
+              allowCompleted={false}
+              filter={filter}
+              minimal={true}
+              onAction={() => {}}
+              onSelect={id => {
+                onSelect(id);
+                close();
+              }}
+              schedules={schedules}
+              statuses={statuses}
+              style={null}
+            />
+          </View>
+        </>
+      )}
     </Modal>
   );
 }
diff --git a/packages/desktop-client/src/hooks/useModalState.ts b/packages/desktop-client/src/hooks/useModalState.ts
new file mode 100644
index 0000000000000000000000000000000000000000..732a69a9623b5fca0e3a16e1cc4e2d348dbd2701
--- /dev/null
+++ b/packages/desktop-client/src/hooks/useModalState.ts
@@ -0,0 +1,44 @@
+import { useCallback } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+
+import { popModal } from 'loot-core/client/actions';
+import { type State } from 'loot-core/client/state-types';
+import { type Modal } from 'loot-core/client/state-types/modals';
+
+type ModalState = {
+  onClose: () => void;
+  modalStack: Modal[];
+  activeModal?: string;
+  isActive: (name: string) => boolean;
+  isHidden: boolean;
+};
+
+export function useModalState(): ModalState {
+  const modalStack = useSelector((state: State) => state.modals.modalStack);
+  const isHidden = useSelector((state: State) => state.modals.isHidden);
+  const dispatch = useDispatch();
+
+  const popModalCallback = useCallback(() => {
+    dispatch(popModal());
+  }, [dispatch]);
+
+  const lastModal = modalStack[modalStack.length - 1];
+  const isActive = useCallback(
+    (name: string) => {
+      if (name === lastModal?.name) {
+        return true;
+      }
+
+      return false;
+    },
+    [lastModal?.name],
+  );
+
+  return {
+    onClose: popModalCallback,
+    modalStack,
+    activeModal: lastModal?.name,
+    isActive,
+    isHidden,
+  };
+}
diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts
index 879092f454e8bba369126d55aa2eb07bcfd02a7d..3d0a23ccee1175811ad8181ec18ab9bf43bac992 100644
--- a/packages/loot-core/src/client/actions/modals.ts
+++ b/packages/loot-core/src/client/actions/modals.ts
@@ -8,6 +8,7 @@ import type {
   ModalWithOptions,
   ModalType,
   FinanceModals,
+  Modal,
 } from '../state-types/modals';
 
 export function pushModal<M extends keyof ModalWithOptions>(
@@ -36,8 +37,7 @@ export function replaceModal<M extends ModalType>(
   name: M,
   options?: FinanceModals[M],
 ): ReplaceModalAction {
-  // @ts-expect-error TS is unable to determine that `name` and `options` match
-  const modal: M = { name, options };
+  const modal: Modal = { name, options };
   return { type: constants.REPLACE_MODAL, modal };
 }
 
diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts
index 3c6b4ab858811fa29ab97023267d1b7e0ec25368..cf801cb2e523455a801587565fb00cc717703f87 100644
--- a/packages/loot-core/src/client/state-types/modals.d.ts
+++ b/packages/loot-core/src/client/state-types/modals.d.ts
@@ -142,6 +142,7 @@ type FinanceModals = {
     onSave: (category: CategoryEntity) => void;
     onEditNotes: (id: string) => void;
     onDelete: (categoryId: string) => void;
+    onToggleVisibility: (categoryId: string) => void;
     onBudgetAction: (month: string, action: string, args?: unknown) => void;
     onClose?: () => void;
   };
@@ -167,6 +168,7 @@ type FinanceModals = {
     onAddCategory: (groupId: string, isIncome: boolean) => void;
     onEditNotes: (id: string) => void;
     onDelete: (groupId: string) => void;
+    onToggleVisibility: (groupId: string) => void;
     onClose?: () => void;
   };
   notes: {
@@ -292,3 +294,9 @@ export type ModalsState = {
   modalStack: Modal[];
   isHidden: boolean;
 };
+
+type Modal = {
+  name: string;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  options?: any;
+};
diff --git a/upcoming-release-notes/2946.md b/upcoming-release-notes/2946.md
new file mode 100644
index 0000000000000000000000000000000000000000..f430704fb4dbef74583ee101ac3901f60e8f235d
--- /dev/null
+++ b/upcoming-release-notes/2946.md
@@ -0,0 +1,6 @@
+---
+category: Maintenance
+authors: [joel-jeremy]
+---
+
+Port finance modals to react-aria-components Modal.