diff --git a/packages/desktop-client/src/ResponsiveProvider.tsx b/packages/desktop-client/src/ResponsiveProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d31ec6e3e3d83815061eab067d8f72a3145f739a
--- /dev/null
+++ b/packages/desktop-client/src/ResponsiveProvider.tsx
@@ -0,0 +1,55 @@
+import { type ReactNode, createContext, useContext } from 'react';
+
+import { useViewportSize } from '@react-aria/utils';
+
+import { breakpoints } from './tokens';
+
+type TResponsiveContext = {
+  atLeastMediumWidth: boolean;
+  isNarrowWidth: boolean;
+  isSmallWidth: boolean;
+  isMediumWidth: boolean;
+  isWideWidth: boolean;
+  height: number;
+  width: number;
+};
+
+const ResponsiveContext = createContext<TResponsiveContext>(null);
+
+export function ResponsiveProvider(props: { children: ReactNode }) {
+  /*
+   * Ensure we render on every viewport size change,
+   * even though we're interested in document.documentElement.client<Width|Height>
+   * clientWidth/Height are the document size, do not change on pinch-zoom,
+   * and are what our `min-width` media queries are reading
+   * Viewport size changes on pinch-zoom, which may be useful later when dealing with on-screen keyboards
+   */
+  useViewportSize();
+
+  const height = document.documentElement.clientHeight;
+  const width = document.documentElement.clientWidth;
+
+  // Possible view modes: narrow, small, medium, wide
+  // To check if we're at least small width, check !isNarrowWidth
+  const viewportInfo = {
+    // atLeastMediumWidth is provided to avoid checking (isMediumWidth || isWideWidth)
+    atLeastMediumWidth: width >= breakpoints.medium,
+    isNarrowWidth: width < breakpoints.small,
+    isSmallWidth: width >= breakpoints.small && width < breakpoints.medium,
+    isMediumWidth: width >= breakpoints.medium && width < breakpoints.wide,
+    // No atLeastWideWidth because that's identical to isWideWidth
+    isWideWidth: width >= breakpoints.wide,
+    height,
+    width,
+  };
+
+  return (
+    <ResponsiveContext.Provider value={viewportInfo}>
+      {props.children}
+    </ResponsiveContext.Provider>
+  );
+}
+
+export function useResponsive() {
+  return useContext(ResponsiveContext);
+}
diff --git a/packages/desktop-client/src/components/App.js b/packages/desktop-client/src/components/App.js
index 3fbc61eb67de0afe19037a1e765abf57ce0b193f..4a314e4f970eb3f84eef91baf7a77c51997494aa 100644
--- a/packages/desktop-client/src/components/App.js
+++ b/packages/desktop-client/src/components/App.js
@@ -10,6 +10,7 @@ import {
 } from 'loot-core/src/platform/client/fetch';
 
 import installPolyfills from '../polyfills';
+import { ResponsiveProvider } from '../ResponsiveProvider';
 import { styles, hasHiddenScrollbars } from '../style';
 
 import AppBackground from './AppBackground';
@@ -90,42 +91,44 @@ class App extends Component {
     const { fatalError, initializing, hiddenScrollbars } = this.state;
 
     return (
-      <div
-        key={hiddenScrollbars ? 'hidden-scrollbars' : 'scrollbars'}
-        {...css([
-          {
-            height: '100%',
-            backgroundColor: '#E8ECF0',
-            overflow: 'hidden',
-          },
-          styles.lightScrollbar,
-        ])}
-      >
-        {fatalError ? (
-          <>
-            <AppBackground />
-            <FatalError error={fatalError} buttonText="Restart app" />
-          </>
-        ) : initializing ? (
-          <AppBackground
-            initializing={initializing}
-            loadingText={loadingText}
-          />
-        ) : budgetId ? (
-          <FinancesApp />
-        ) : (
-          <>
+      <ResponsiveProvider>
+        <div
+          key={hiddenScrollbars ? 'hidden-scrollbars' : 'scrollbars'}
+          {...css([
+            {
+              height: '100%',
+              backgroundColor: '#E8ECF0',
+              overflow: 'hidden',
+            },
+            styles.lightScrollbar,
+          ])}
+        >
+          {fatalError ? (
+            <>
+              <AppBackground />
+              <FatalError error={fatalError} buttonText="Restart app" />
+            </>
+          ) : initializing ? (
             <AppBackground
               initializing={initializing}
               loadingText={loadingText}
             />
-            <ManagementApp isLoading={loadingText != null} />
-          </>
-        )}
-
-        <UpdateNotification />
-        <MobileWebMessage />
-      </div>
+          ) : budgetId ? (
+            <FinancesApp />
+          ) : (
+            <>
+              <AppBackground
+                initializing={initializing}
+                loadingText={loadingText}
+              />
+              <ManagementApp isLoading={loadingText != null} />
+            </>
+          )}
+
+          <UpdateNotification />
+          <MobileWebMessage />
+        </div>
+      </ResponsiveProvider>
     );
   }
 }
diff --git a/packages/desktop-client/src/components/FinancesApp.js b/packages/desktop-client/src/components/FinancesApp.js
index 61e4cfa12ada23b911d65d5a905087c264f2b54a..6ec499df2470855b277bc26c311fc9a3b5cb09e8 100644
--- a/packages/desktop-client/src/components/FinancesApp.js
+++ b/packages/desktop-client/src/components/FinancesApp.js
@@ -1,4 +1,4 @@
-import React, { Component, useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import { DndProvider } from 'react-dnd';
 import Backend from 'react-dnd-html5-backend';
 import { connect } from 'react-redux';
@@ -26,19 +26,19 @@ import * as undo from 'loot-core/src/platform/client/undo';
 import Cog from '../icons/v1/Cog';
 import PiggyBank from '../icons/v1/PiggyBank';
 import Wallet from '../icons/v1/Wallet';
+import { useResponsive } from '../ResponsiveProvider';
 import { colors, styles } from '../style';
-import { isMobile } from '../util';
 import { getLocationState, makeLocationState } from '../util/location-state';
 import { getIsOutdated, getLatestVersion } from '../util/versions';
 
 import Account from './accounts/Account';
-import { default as MobileAccount } from './accounts/MobileAccount';
-import { default as MobileAccounts } from './accounts/MobileAccounts';
+import MobileAccount from './accounts/MobileAccount';
+import MobileAccounts from './accounts/MobileAccounts';
 import { ActiveLocationProvider } from './ActiveLocation';
 import BankSyncStatus from './BankSyncStatus';
 import Budget from './budget';
 import { BudgetMonthCountProvider } from './budget/BudgetMonthCountContext';
-import { default as MobileBudget } from './budget/MobileBudget';
+import MobileBudget from './budget/MobileBudget';
 import { View } from './common';
 import FloatableSidebar, { SidebarProvider } from './FloatableSidebar';
 import GlobalKeys from './GlobalKeys';
@@ -57,8 +57,14 @@ import PostsOfflineNotification from './schedules/PostsOfflineNotification';
 import Settings from './settings';
 import Titlebar, { TitlebarProvider } from './Titlebar';
 
-function PageRoute({ path, component: Component }) {
-  return (
+function PageRoute({
+  path,
+  component: Component,
+  redirectTo = '/budget',
+  worksInNarrow = true,
+}) {
+  const { isNarrowWidth } = useResponsive();
+  return worksInNarrow || !isNarrowWidth ? (
     <Route
       path={path}
       children={props => {
@@ -74,52 +80,100 @@ function PageRoute({ path, component: Component }) {
         );
       }}
     />
+  ) : (
+    <Redirect to={redirectTo} />
   );
 }
 
-function Routes({ isMobile, location }) {
+function NonPageRoute({
+  redirectTo = '/budget',
+  worksInNarrow = true,
+  ...routeProps
+}) {
+  const { isNarrowWidth } = useResponsive();
+
+  return worksInNarrow || !isNarrowWidth ? (
+    <Route {...routeProps} />
+  ) : (
+    <Redirect to={redirectTo} />
+  );
+}
+
+function Routes({ location }) {
+  const { isNarrowWidth } = useResponsive();
   return (
     <Switch location={location}>
-      <Route path="/" exact render={() => <Redirect to="/budget" />} />
+      <Redirect from="/" exact to="/budget" />
 
-      <PageRoute path="/reports" component={Reports} />
-      <PageRoute path="/budget" component={isMobile ? MobileBudget : Budget} />
+      <PageRoute path="/reports" component={Reports} worksInNarrow={false} />
 
-      <Route path="/schedules" exact component={Schedules} />
-      <Route path="/schedule/edit" exact component={EditSchedule} />
-      <Route path="/schedule/edit/:id" component={EditSchedule} />
-      <Route path="/schedule/link" component={LinkSchedule} />
-      <Route path="/schedule/discover" component={DiscoverSchedules} />
-      <Route
+      <PageRoute
+        path="/budget"
+        component={isNarrowWidth ? MobileBudget : Budget}
+      />
+
+      <NonPageRoute
+        path="/schedules"
+        exact
+        component={Schedules}
+        worksInNarrow={false}
+      />
+      <NonPageRoute
+        path="/schedule/edit"
+        exact
+        component={EditSchedule}
+        worksInNarrow={false}
+      />
+      <NonPageRoute
+        path="/schedule/edit/:id"
+        component={EditSchedule}
+        worksInNarrow={false}
+      />
+      <NonPageRoute
+        path="/schedule/link"
+        component={LinkSchedule}
+        worksInNarrow={false}
+      />
+      <NonPageRoute
+        path="/schedule/discover"
+        component={DiscoverSchedules}
+        worksInNarrow={false}
+      />
+      <NonPageRoute
         path="/schedule/posts-offline-notification"
         component={PostsOfflineNotification}
       />
 
-      <Route path="/payees" exact component={ManagePayeesPage} />
-      <Route path="/rules" exact component={ManageRulesPage} />
-      <Route path="/settings" component={Settings} />
-      <Route path="/nordigen/link" exact component={NordigenLink} />
+      <NonPageRoute path="/payees" exact component={ManagePayeesPage} />
+      <NonPageRoute path="/rules" exact component={ManageRulesPage} />
+      <NonPageRoute path="/settings" component={Settings} />
+      <NonPageRoute
+        path="/nordigen/link"
+        exact
+        component={NordigenLink}
+        worksInNarrow={false}
+      />
 
-      <Route
+      <NonPageRoute
         path="/accounts/:id"
         exact
         children={props => {
-          const AcctCmp = isMobile ? MobileAccount : Account;
+          const AcctCmp = isNarrowWidth ? MobileAccount : Account;
           return (
             props.match && <AcctCmp key={props.match.params.id} {...props} />
           );
         }}
       />
-      <Route
+      <NonPageRoute
         path="/accounts"
         exact
-        component={isMobile ? MobileAccounts : Account}
+        component={isNarrowWidth ? MobileAccounts : Account}
       />
     </Switch>
   );
 }
 
-function StackedRoutes({ isMobile }) {
+function StackedRoutes() {
   let location = useLocation();
   let locationPtr = getLocationState(location, 'locationPtr');
 
@@ -134,14 +188,14 @@ function StackedRoutes({ isMobile }) {
 
   return (
     <ActiveLocationProvider location={locations[locations.length - 1]}>
-      <Routes location={base} isMobile={isMobile} />
+      <Routes location={base} />
       {stack.map((location, idx) => (
         <PageTypeProvider
           key={location.key}
           type="modal"
           current={idx === stack.length - 1}
         >
-          <Routes location={location} isMobile={isMobile} />
+          <Routes location={location} />
         </PageTypeProvider>
       ))}
     </ActiveLocationProvider>
@@ -172,6 +226,7 @@ function NavTab({ icon: TabIcon, name, path }) {
 }
 
 function MobileNavTabs() {
+  const { isNarrowWidth } = useResponsive();
   return (
     <div
       style={{
@@ -179,71 +234,60 @@ function MobileNavTabs() {
         borderTop: `1px solid ${colors.n10}`,
         bottom: 0,
         ...styles.shadow,
-        display: 'flex',
+        display: isNarrowWidth ? 'flex' : 'none',
         height: '80px',
         justifyContent: 'space-around',
         paddingTop: 10,
         width: '100%',
       }}
     >
-      <NavTab name="Budget" path="/budget" icon={Wallet} isActive={false} />
-      <NavTab
-        name="Accounts"
-        path="/accounts"
-        icon={PiggyBank}
-        isActive={false}
-      />
-      <NavTab name="Settings" path="/settings" icon={Cog} isActive={false} />
+      <NavTab name="Budget" path="/budget" icon={Wallet} />
+      <NavTab name="Accounts" path="/accounts" icon={PiggyBank} />
+      <NavTab name="Settings" path="/settings" icon={Cog} />
     </div>
   );
 }
 
-class FinancesApp extends Component {
-  constructor(props) {
-    super(props);
-    this.state = { isMobile: isMobile() };
-    this.history = createBrowserHistory();
+function FinancesApp(props) {
+  const [patchedHistory] = useState(() => createBrowserHistory());
 
-    let oldPush = this.history.push;
-    this.history.push = (to, state) => {
+  useEffect(() => {
+    let oldPush = patchedHistory.push;
+    patchedHistory.push = (to, state) => {
       let newState = makeLocationState(to.state || state);
       if (typeof to === 'object') {
-        return oldPush.call(this.history, { ...to, state: newState });
+        return oldPush.call(patchedHistory, { ...to, state: newState });
       } else {
-        return oldPush.call(this.history, to, newState);
+        return oldPush.call(patchedHistory, to, newState);
       }
     };
 
     // I'm not sure if this is the best approach but we need this to
     // globally. We could instead move various workflows inside global
     // React components, but that's for another day.
-    window.__history = this.history;
+    window.__history = patchedHistory;
 
     undo.setUndoState('url', window.location.href);
 
-    this.cleanup = this.history.listen(location => {
+    const cleanup = patchedHistory.listen(location => {
       undo.setUndoState('url', window.location.href);
     });
 
-    this.handleWindowResize = this.handleWindowResize.bind(this);
-  }
-
-  handleWindowResize() {
-    this.setState({ isMobile: isMobile() });
-  }
+    return cleanup;
+  }, []);
 
-  componentDidMount() {
+  useEffect(() => {
     // TODO: quick hack fix for showing the demo
-    if (this.history.location.pathname === '/subscribe') {
-      this.history.push('/');
+    if (patchedHistory.location.pathname === '/subscribe') {
+      patchedHistory.push('/');
     }
 
     // Get the accounts and check if any exist. If there are no
     // accounts, we want to redirect the user to the All Accounts
     // screen which will prompt them to add an account
-    this.props.getAccounts().then(accounts => {
+    props.getAccounts().then(accounts => {
       if (accounts.length === 0) {
-        this.history.push('/accounts');
+        patchedHistory.push('/accounts');
       }
     });
 
@@ -253,96 +297,81 @@ class FinancesApp extends Component {
     // Wait a little bit to make sure the sync button will get the
     // sync start event. This can be improved later.
     setTimeout(async () => {
-      await this.props.sync();
+      await props.sync();
 
       // Check for upgrade notifications. We do this after syncing
       // because these states are synced across devices, so they will
       // only see it once for this file
       checkForUpgradeNotifications(
-        this.props.addNotification,
-        this.props.resetSync,
-        this.history,
+        props.addNotification,
+        props.resetSync,
+        patchedHistory,
       );
-    }, 100);
 
-    setTimeout(async () => {
-      await this.props.sync();
       await checkForUpdateNotification(
-        this.props.addNotification,
+        props.addNotification,
         getIsOutdated,
         getLatestVersion,
-        this.props.loadPrefs,
-        this.props.savePrefs,
+        props.loadPrefs,
+        props.savePrefs,
       );
     }, 100);
+  }, []);
 
-    window.addEventListener('resize', this.handleWindowResize);
-  }
-
-  componentWillUnmount() {
-    this.cleanup();
-    window.removeEventListener('resize', this.handleWindowResize);
-  }
-
-  render() {
-    return (
-      <Router history={this.history}>
-        <CompatRouter>
-          <View style={{ height: '100%', backgroundColor: colors.n10 }}>
-            <GlobalKeys />
-
-            <View style={{ flexDirection: 'row', flex: 1 }}>
-              {!this.state.isMobile && <FloatableSidebar />}
-
+  return (
+    <Router history={patchedHistory}>
+      <CompatRouter>
+        <View style={{ height: '100%', backgroundColor: colors.n10 }}>
+          <GlobalKeys />
+
+          <View style={{ flexDirection: 'row', flex: 1 }}>
+            <FloatableSidebar />
+
+            <div
+              style={{
+                flex: 1,
+                display: 'flex',
+                flexDirection: 'column',
+                overflow: 'hidden',
+                position: 'relative',
+                width: '100%',
+              }}
+            >
+              <Titlebar
+                style={{
+                  WebkitAppRegion: 'drag',
+                  position: 'absolute',
+                  top: 0,
+                  left: 0,
+                  right: 0,
+                  zIndex: 1000,
+                }}
+              />
               <div
                 style={{
                   flex: 1,
                   display: 'flex',
-                  flexDirection: 'column',
-                  overflow: 'hidden',
+                  overflow: 'auto',
                   position: 'relative',
-                  width: '100%',
                 }}
               >
-                {!this.state.isMobile && (
-                  <Titlebar
-                    style={{
-                      WebkitAppRegion: 'drag',
-                      position: 'absolute',
-                      top: 0,
-                      left: 0,
-                      right: 0,
-                      zIndex: 1000,
-                    }}
-                  />
-                )}
-                <div
-                  style={{
-                    flex: 1,
-                    display: 'flex',
-                    overflow: 'auto',
-                    position: 'relative',
-                  }}
-                >
-                  <Notifications />
-                  <BankSyncStatus />
-                  <StackedRoutes isMobile={this.state.isMobile} />
-                  <Modals history={this.history} />
-                </div>
-                {this.state.isMobile && (
-                  <Switch>
-                    <Route path="/budget" component={MobileNavTabs} />
-                    <Route path="/accounts" component={MobileNavTabs} />
-                    <Route path="/settings" component={MobileNavTabs} />
-                  </Switch>
-                )}
+                <Notifications />
+                <BankSyncStatus />
+                <StackedRoutes />
+                <Modals history={patchedHistory} />
               </div>
-            </View>
+
+              <Switch>
+                <Route path="/budget" component={MobileNavTabs} />
+                <Route path="/accounts" component={MobileNavTabs} />
+                <Route path="/settings" component={MobileNavTabs} />
+              </Switch>
+            </div>
           </View>
-        </CompatRouter>
-      </Router>
-    );
-  }
+        </View>
+      </CompatRouter>
+    </Router>
+  );
 }
 
 function FinancesAppWithContext(props) {
diff --git a/packages/desktop-client/src/components/FloatableSidebar.js b/packages/desktop-client/src/components/FloatableSidebar.js
index 03f685586ef9e7f19a176780083cd2187fd4dcb7..444ee866e59a73a46a4a94bea0984feec93a04aa 100644
--- a/packages/desktop-client/src/components/FloatableSidebar.js
+++ b/packages/desktop-client/src/components/FloatableSidebar.js
@@ -1,11 +1,11 @@
 import React, { createContext, useState, useContext, useMemo } from 'react';
-import { connect } from 'react-redux';
+import { connect, useSelector } from 'react-redux';
 import { withRouter } from 'react-router-dom';
 
-import { useViewportSize } from '@react-aria/utils';
-
 import * as actions from 'loot-core/src/client/actions';
 
+import { useResponsive } from '../ResponsiveProvider';
+
 import { View } from './common';
 import { SIDEBAR_WIDTH } from './sidebar';
 import SidebarWithData from './SidebarWithData';
@@ -13,32 +13,40 @@ import SidebarWithData from './SidebarWithData';
 const SidebarContext = createContext(null);
 
 export function SidebarProvider({ children }) {
+  let floatingSidebar = useSelector(
+    state => state.prefs.global.floatingSidebar,
+  );
   let [hidden, setHidden] = useState(true);
+  let { width } = useResponsive();
+  let alwaysFloats = width < 668;
+  let floating = floatingSidebar || alwaysFloats;
+
   return (
-    <SidebarContext.Provider value={[hidden, setHidden]}>
+    <SidebarContext.Provider
+      value={{ hidden, setHidden, floating, alwaysFloats }}
+    >
       {children}
     </SidebarContext.Provider>
   );
 }
 
 export function useSidebar() {
-  useViewportSize(); // Force re-render on window resize
-  let windowWidth = document.documentElement.clientWidth;
-  let alwaysFloats = windowWidth < 668;
+  let { hidden, setHidden, floating, alwaysFloats } =
+    useContext(SidebarContext);
 
-  let [hidden, setHidden] = useContext(SidebarContext);
   return useMemo(
-    () => ({ hidden, setHidden, alwaysFloats }),
-    [hidden, setHidden, alwaysFloats],
+    () => ({ hidden, setHidden, floating, alwaysFloats }),
+    [hidden, setHidden, floating, alwaysFloats],
   );
 }
 
 function Sidebar({ floatingSidebar }) {
   let sidebar = useSidebar();
+  let { isNarrowWidth } = useResponsive();
 
   let sidebarShouldFloat = floatingSidebar || sidebar.alwaysFloats;
 
-  return (
+  return isNarrowWidth ? null : (
     <View
       onMouseOver={
         sidebarShouldFloat
diff --git a/packages/desktop-client/src/components/MobileWebMessage.js b/packages/desktop-client/src/components/MobileWebMessage.js
index e74a4637d2de311323007bd55302b2fd375721c4..71296e4df98d803d3f2e2f8ee114c5a2277aeab6 100644
--- a/packages/desktop-client/src/components/MobileWebMessage.js
+++ b/packages/desktop-client/src/components/MobileWebMessage.js
@@ -3,8 +3,8 @@ import { useDispatch, useSelector } from 'react-redux';
 
 import { savePrefs } from 'loot-core/src/client/actions';
 
+import { useResponsive } from '../ResponsiveProvider';
 import { colors, styles } from '../style';
-import { isMobile } from '../util';
 
 import { View, Text, Button } from './common';
 import { Checkbox } from './forms';
@@ -16,8 +16,10 @@ export default function MobileWebMessage() {
     return (state.prefs.local && state.prefs.local.hideMobileMessage) || true;
   });
 
+  const { isNarrowWidth } = useResponsive();
+
   let [show, setShow] = useState(
-    isMobile() &&
+    isNarrowWidth &&
       !hideMobileMessagePref &&
       !document.cookie.match(/hideMobileMessage=true/),
   );
diff --git a/packages/desktop-client/src/components/Page.js b/packages/desktop-client/src/components/Page.js
index 7c78170629be3698c89288f505cd1d0f854dcfce..ccacf653ba1be94ed5c1abaaeff6d46e84c6cfb1 100644
--- a/packages/desktop-client/src/components/Page.js
+++ b/packages/desktop-client/src/components/Page.js
@@ -1,15 +1,13 @@
 import React, { createContext, useContext } from 'react';
 import { useHistory } from 'react-router-dom';
 
+import { useResponsive } from '../ResponsiveProvider';
 import { colors, styles } from '../style';
-import { isMobile } from '../util';
 
 import { Modal, View, Text } from './common';
 
 let PageTypeContext = createContext({ type: 'page' });
 
-const HORIZONTAL_PADDING = isMobile() ? 10 : 20;
-
 export function PageTypeProvider({ type, current, children }) {
   return (
     <PageTypeContext.Provider value={{ type, current }}>
@@ -23,7 +21,9 @@ export function usePageType() {
 }
 
 function PageTitle({ name, style }) {
-  if (isMobile()) {
+  const { isNarrowWidth } = useResponsive();
+
+  if (isNarrowWidth) {
     return (
       <View
         style={[
@@ -53,8 +53,6 @@ function PageTitle({ name, style }) {
         {
           fontSize: 25,
           fontWeight: 500,
-          paddingLeft: HORIZONTAL_PADDING,
-          paddingRight: HORIZONTAL_PADDING,
           marginBottom: 15,
         },
         style,
@@ -68,6 +66,8 @@ function PageTitle({ name, style }) {
 export function Page({ title, modalSize, children, titleStyle }) {
   let { type, current } = usePageType();
   let history = useHistory();
+  let { isNarrowWidth } = useResponsive();
+  let HORIZONTAL_PADDING = isNarrowWidth ? 10 : 20;
 
   if (type === 'modal') {
     let size = modalSize;
@@ -89,11 +89,17 @@ export function Page({ title, modalSize, children, titleStyle }) {
   }
 
   return (
-    <View style={isMobile() ? undefined : styles.page}>
-      <PageTitle name={title} style={titleStyle} />
+    <View style={isNarrowWidth ? undefined : styles.page}>
+      <PageTitle
+        name={title}
+        style={{
+          ...titleStyle,
+          paddingInline: HORIZONTAL_PADDING,
+        }}
+      />
       <View
         style={
-          isMobile()
+          isNarrowWidth
             ? { overflowY: 'auto', padding: HORIZONTAL_PADDING }
             : {
                 paddingLeft: HORIZONTAL_PADDING,
diff --git a/packages/desktop-client/src/components/Titlebar.js b/packages/desktop-client/src/components/Titlebar.js
index 2aff10480b825c5b6a484177cd5f836979983464..1b80258add2162171521297b0408b5e7f6ff2065 100644
--- a/packages/desktop-client/src/components/Titlebar.js
+++ b/packages/desktop-client/src/components/Titlebar.js
@@ -19,6 +19,7 @@ import useFeatureFlag from '../hooks/useFeatureFlag';
 import ArrowLeft from '../icons/v1/ArrowLeft';
 import AlertTriangle from '../icons/v2/AlertTriangle';
 import NavigationMenu from '../icons/v2/NavigationMenu';
+import { useResponsive } from '../ResponsiveProvider';
 import { colors } from '../style';
 import tokens from '../tokens';
 
@@ -136,7 +137,7 @@ export function SyncButton({ localPrefs, style, onSync }) {
               ? colors.n9
               : null,
         },
-        media(`(min-width: ${tokens.breakpoint_medium})`, {
+        media(`(min-width: ${tokens.breakpoint_small})`, {
           color:
             syncState === 'error'
               ? colors.r4
@@ -269,9 +270,10 @@ function Titlebar({
   sync,
 }) {
   let sidebar = useSidebar();
+  let { isNarrowWidth } = useResponsive();
   const serverURL = useServerURL();
 
-  return (
+  return isNarrowWidth ? null : (
     <View
       style={[
         {
diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx
index bdfcc9f4c6005dae11a2cf03f3f7ce384281d469..8c99af242b60401c2861a4782f125627b27c8d23 100644
--- a/packages/desktop-client/src/components/common/Modal.tsx
+++ b/packages/desktop-client/src/components/common/Modal.tsx
@@ -126,8 +126,8 @@ const Modal = ({
             borderRadius: 4,
             backgroundColor: 'white',
             opacity: isHidden ? 0 : 1,
-            [`@media (min-width: ${tokens.breakpoint_narrow})`]: {
-              minWidth: tokens.breakpoint_narrow,
+            [`@media (min-width: ${tokens.breakpoint_small})`]: {
+              minWidth: tokens.breakpoint_small,
             },
           },
           styles.shadowLarge,
diff --git a/packages/desktop-client/src/components/manager/BudgetList.js b/packages/desktop-client/src/components/manager/BudgetList.js
index 8957425faee6db062d071d318986c1af8b2b5c9c..4e6a9f9988b9df27758dacc300f6d4d695c71365 100644
--- a/packages/desktop-client/src/components/manager/BudgetList.js
+++ b/packages/desktop-client/src/components/manager/BudgetList.js
@@ -197,7 +197,7 @@ function BudgetTable({ files, onSelect, onDelete }) {
     <View
       style={{
         flexGrow: 1,
-        [`@media (min-width: ${tokens.breakpoint_narrow})`]: {
+        [`@media (min-width: ${tokens.breakpoint_small})`]: {
           flexGrow: 0,
           maxHeight: 310,
         },
@@ -261,12 +261,12 @@ function BudgetList({
       style={{
         flex: 1,
         justifyContent: 'center',
-        minWidth: tokens.breakpoint_narrow,
-        [`@media (max-width: ${tokens.breakpoint_narrow})`]: {
-          width: '100vw',
-          minWidth: '100vw',
-          marginInline: -20,
-          marginTop: 20,
+        marginInline: -20,
+        marginTop: 20,
+        width: '100vw',
+        [`@media (min-width: ${tokens.breakpoint_small})`]: {
+          maxWidth: tokens.breakpoint_small,
+          width: '100%',
         },
       }}
     >
diff --git a/packages/desktop-client/src/components/manager/ManagementApp.js b/packages/desktop-client/src/components/manager/ManagementApp.js
index 682fe0a97f2b7be54381e3f55ad504184d66aafe..d5724e0da42ee41697ac89584e558a1fe5956031 100644
--- a/packages/desktop-client/src/components/manager/ManagementApp.js
+++ b/packages/desktop-client/src/components/manager/ManagementApp.js
@@ -33,7 +33,7 @@ function Version() {
         ':hover': { color: colors.n2 },
         margin: 15,
         marginLeft: 17,
-        [`@media (min-width: ${tokens.breakpoint_medium})`]: {
+        [`@media (min-width: ${tokens.breakpoint_small})`]: {
           position: 'absolute',
           bottom: 0,
           right: 0,
diff --git a/packages/desktop-client/src/components/settings/Format.js b/packages/desktop-client/src/components/settings/Format.js
index ce27e3d6239ebd39e043efdefc1019d9ae33e1f8..50e0b8ad71d807d01c4663e2ef3335045804ddeb 100644
--- a/packages/desktop-client/src/components/settings/Format.js
+++ b/packages/desktop-client/src/components/settings/Format.js
@@ -4,6 +4,7 @@ import { numberFormats } from 'loot-core/src/shared/util';
 
 import tokens from '../../tokens';
 import { Button, CustomSelect, Text, View } from '../common';
+import { useSidebar } from '../FloatableSidebar';
 import { Checkbox } from '../forms';
 
 import { Setting } from './UI';
@@ -33,11 +34,9 @@ function Column({ title, children }) {
     <View
       style={{
         alignItems: 'flex-start',
-        gap: '0.5em',
         flexGrow: 1,
-        [`@media (max-width: ${tokens.breakpoint_xs})`]: {
-          width: '100%',
-        },
+        gap: '0.5em',
+        width: '100%',
       }}
     >
       <Text style={{ fontWeight: 500 }}>{title}</Text>
@@ -64,6 +63,7 @@ export default function FormatSettings({ prefs, savePrefs }) {
     savePrefs({ hideFraction });
   }
 
+  let sidebar = useSidebar();
   let firstDayOfWeekIdx = prefs.firstDayOfWeekIdx || '0'; // Sunday
   let dateFormat = prefs.dateFormat || 'MM/dd/yyyy';
   let numberFormat = prefs.numberFormat || 'comma-dot';
@@ -73,11 +73,15 @@ export default function FormatSettings({ prefs, savePrefs }) {
       primaryAction={
         <View
           style={{
-            flexDirection: 'row',
+            flexDirection: 'column',
             gap: '1em',
             width: '100%',
-            [`@media (max-width: ${tokens.breakpoint_xs})`]: {
-              flexDirection: 'column',
+            [`@media (min-width: ${
+              sidebar.floating
+                ? tokens.breakpoint_small
+                : tokens.breakpoint_medium
+            })`]: {
+              flexDirection: 'row',
             },
           }}
         >
diff --git a/packages/desktop-client/src/components/settings/UI.tsx b/packages/desktop-client/src/components/settings/UI.tsx
index d64897fd9bd6acf58c38d6af9861820d97704329..40d39221dbb01798cbf70e38dfe9f96027fc5cd0 100644
--- a/packages/desktop-client/src/components/settings/UI.tsx
+++ b/packages/desktop-client/src/components/settings/UI.tsx
@@ -63,7 +63,7 @@ export const AdvancedToggle = ({ children }: AdvancedToggleProps) => {
           marginBottom: 25,
           width: '100%',
         },
-        media(`(min-width: ${tokens.breakpoint_medium})`, {
+        media(`(min-width: ${tokens.breakpoint_small})`, {
           width: 'auto',
         }),
       ]}
diff --git a/packages/desktop-client/src/components/settings/index.js b/packages/desktop-client/src/components/settings/index.js
index 2c1d0502f28fa873759942aef4621736086506ed..c56f0775e4f6ccffd8a4632ff598e4eb0e15ae10 100644
--- a/packages/desktop-client/src/components/settings/index.js
+++ b/packages/desktop-client/src/components/settings/index.js
@@ -8,9 +8,9 @@ import * as Platform from 'loot-core/src/client/platform';
 import { listen } from 'loot-core/src/platform/client/fetch';
 
 import useLatestVersion, { useIsOutdated } from '../../hooks/useLatestVersion';
+import { useResponsive } from '../../ResponsiveProvider';
 import { colors } from '../../style';
 import tokens from '../../tokens';
-import { isMobile } from '../../util';
 import { withThemeColor } from '../../util/withThemeColor';
 import { View, Text, Button, Input } from '../common';
 import { FormField, FormLabel } from '../forms';
@@ -40,7 +40,7 @@ function About() {
       <View
         style={[
           { flexDirection: 'column', gap: 10 },
-          media(`(min-width: ${tokens.breakpoint_medium})`, {
+          media(`(min-width: ${tokens.breakpoint_small})`, {
             display: 'grid',
             gridTemplateRows: '1fr 1fr',
             gridTemplateColumns: '50% 50%',
@@ -122,16 +122,19 @@ function Settings({
     return () => unlisten();
   }, [loadPrefs]);
 
+  const { isNarrowWidth } = useResponsive();
+
   return (
     <View
       style={{
-        marginInline: globalPrefs.floatingSidebar && !isMobile() ? 'auto' : 0,
+        marginInline:
+          globalPrefs.floatingSidebar && !isNarrowWidth ? 'auto' : 0,
       }}
     >
       <Page
         title="Settings"
         titleStyle={
-          isMobile()
+          isNarrowWidth
             ? {
                 backgroundColor: colors.n11,
                 color: colors.n1,
@@ -140,7 +143,7 @@ function Settings({
         }
       >
         <View style={{ flexShrink: 0, gap: 30 }}>
-          {isMobile() && (
+          {isNarrowWidth && (
             <View
               style={{ gap: 10, flexDirection: 'row', alignItems: 'flex-end' }}
             >
diff --git a/packages/desktop-client/src/style.tsx b/packages/desktop-client/src/style.tsx
index 7d5d61cc9d94105e416c80a2a3a22b6619eef645..84fa957e759efe1fb1848548bad4dc57a242245f 100644
--- a/packages/desktop-client/src/style.tsx
+++ b/packages/desktop-client/src/style.tsx
@@ -97,7 +97,7 @@ export const styles = {
   },
   smallText: {
     fontSize: 13,
-    [`@media (min-width: ${tokens.breakpoint_medium})`]: {
+    [`@media (min-width: ${tokens.breakpoint_small})`]: {
       // lineHeight: 21 // TODO: This seems like trouble, but what's the right value?
     },
   },
@@ -105,16 +105,12 @@ export const styles = {
     fontSize: 13,
   },
   page: {
-    // This is the height of the titlebar
-    paddingTop: 8,
     flex: 1,
-    [`@media (min-width: ${tokens.breakpoint_xs})`]: {
-      minWidth: 360,
+    minHeight: 700, // ensure we can scroll on small screens
+    paddingTop: 8, // height of the titlebar
+    [`@media (min-width: ${tokens.breakpoint_small})`]: {
       paddingTop: 36,
     },
-    [`@media (min-width: ${tokens.breakpoint_medium})`]: {
-      minWidth: 500,
-    },
   },
   pageHeader: {
     fontSize: 25,
@@ -129,14 +125,14 @@ export const styles = {
   pageContent: {
     paddingLeft: 2,
     paddingRight: 2,
-    [`@media (min-width: ${tokens.breakpoint_medium})`]: {
+    [`@media (min-width: ${tokens.breakpoint_small})`]: {
       paddingLeft: 20,
       paddingRight: 20,
     },
   },
   settingsPageContent: {
     padding: 20,
-    [`@media (min-width: ${tokens.breakpoint_medium})`]: {
+    [`@media (min-width: ${tokens.breakpoint_small})`]: {
       padding: 'inherit',
     },
   },
diff --git a/packages/desktop-client/src/tokens.js b/packages/desktop-client/src/tokens.js
deleted file mode 100644
index de4c6eec4e3d454f745461d35b69d1fa042bb687..0000000000000000000000000000000000000000
--- a/packages/desktop-client/src/tokens.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const tokens = {
-  breakpoint_xs: '350px',
-  breakpoint_narrow: '512px',
-  breakpoint_medium: '768px',
-  breakpoint_wide: '1024px',
-};
-export default tokens;
-
-export const breakpoints = {
-  xs: 350,
-  narrow: 512,
-  medium: 768,
-  wide: 1024,
-};
diff --git a/packages/desktop-client/src/tokens.ts b/packages/desktop-client/src/tokens.ts
new file mode 100644
index 0000000000000000000000000000000000000000..280dc7f9eb7cd96b1fc1fe2f9ab4875f05a7f4d7
--- /dev/null
+++ b/packages/desktop-client/src/tokens.ts
@@ -0,0 +1,34 @@
+enum BreakpointNames {
+  small = 'small',
+  medium = 'medium',
+  wide = 'wide',
+}
+
+type NumericBreakpoints = {
+  [key in BreakpointNames]: number;
+};
+
+export const breakpoints: NumericBreakpoints = {
+  small: 512,
+  medium: 730,
+  wide: 1100,
+};
+
+type BreakpointsPx = {
+  [B in keyof NumericBreakpoints as `breakpoint_${B}`]: string;
+};
+
+// Provide the same breakpoints in a form usable by CSS media queries
+// {
+//   breakpoint_small: '512px',
+//   breakpoint_medium: '740px',
+//   breakpoint_wide: '1100px',
+// }
+const breakpointsInPx: BreakpointsPx = Object.entries(
+  breakpoints,
+).reduce<BreakpointsPx>((acc, [key, val]) => {
+  acc[`breakpoint_${key}`] = `${val}px`;
+  return acc;
+}, {} as BreakpointsPx);
+
+export default breakpointsInPx;
diff --git a/packages/desktop-client/src/util.js b/packages/desktop-client/src/util.js
index bde05fce7d48b3cc9cef1c412aa94837b5eb5701..35e1fadd11506164e30c21d9c2fa24526385daf3 100644
--- a/packages/desktop-client/src/util.js
+++ b/packages/desktop-client/src/util.js
@@ -2,9 +2,3 @@ export function getModalRoute(name) {
   let parts = name.split('/');
   return [parts[0], parts.slice(1).join('/')];
 }
-
-export function isMobile() {
-  let details = navigator.userAgent;
-  let regexp = /Mobi|android|iphone|kindle|ipad/i;
-  return regexp.test(details);
-}
diff --git a/upcoming-release-notes/964.md b/upcoming-release-notes/964.md
new file mode 100644
index 0000000000000000000000000000000000000000..be23c35bb1e59896d6c6536ca5e8c27c74ba6032
--- /dev/null
+++ b/upcoming-release-notes/964.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [trevdor]
+---
+
+Introduces a ResponsiveProvider as a foundation for future mobile development. Makes transaction entry available to many mobile users in landscape orientation.