diff --git a/src/shared/components/Modal.tsx b/src/shared/components/Modal.tsx
index 74d0043a77d1..503c22a81fb3 100644
--- a/src/shared/components/Modal.tsx
+++ b/src/shared/components/Modal.tsx
@@ -60,12 +60,18 @@ const TOASTER_SELECTOR = '[data-slot="toaster"]';
const isFromToaster = (event: Event) =>
event.target instanceof Element && event.target.closest(TOASTER_SELECTOR) !== null;
+// Dialogs take ~70% of the viewport from the `sm` breakpoint up, so settings-style modals
+// (API key permissions, provider config, routing editors) stop cramming their forms into a
+// 28rem column. Below `sm` they keep the kit's `calc(100vw - 3rem)` width with its 30rem cap
+// lifted (`max-w-none`), i.e. full width minus the mobile gutter. Only `sm` keeps a fixed
+// narrow width — it is the confirm/alert dialog size.
+const WIDE = "max-w-none sm:w-[70vw] sm:max-w-[70vw]";
const SIZES = {
sm: "max-w-sm",
- md: "max-w-md",
- lg: "max-w-lg",
- xl: "max-w-xl",
- full: "max-w-4xl",
+ md: WIDE,
+ lg: WIDE,
+ xl: WIDE,
+ full: WIDE,
} as const;
// The element focused right now, unless it is
(nothing to return to).
diff --git a/tests/unit/ui/modal-width-70vw.test.tsx b/tests/unit/ui/modal-width-70vw.test.tsx
new file mode 100644
index 000000000000..fd556a191bbb
--- /dev/null
+++ b/tests/unit/ui/modal-width-70vw.test.tsx
@@ -0,0 +1,67 @@
+// @vitest-environment jsdom
+import React, { act } from "react";
+import { createRoot } from "react-dom/client";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+vi.mock("next-intl", () => ({
+ useTranslations: () => (key: string) => key,
+}));
+
+const { default: Modal } = await import("../../../src/shared/components/Modal");
+
+const cleanups: Array<() => void> = [];
+
+function renderModal(props: Partial> = {}) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+ const root = createRoot(container);
+ (
+ globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
+ ).IS_REACT_ACT_ENVIRONMENT = true;
+ act(() =>
+ root.render(
+ {}} title="Width" {...props}>
+ body
+
+ )
+ );
+ cleanups.push(() => {
+ act(() => root.unmount());
+ container.remove();
+ });
+ // The kit Dialog (Radix) portals its content to , outside `container`.
+ const dialogs = document.body.querySelectorAll('[role="dialog"]');
+ return dialogs[dialogs.length - 1];
+}
+
+afterEach(() => {
+ while (cleanups.length) cleanups.pop()!();
+});
+
+describe("Modal width", () => {
+ it("defaults (md) to ~70% of the viewport from the sm breakpoint, full width below", () => {
+ const dialog = renderModal();
+ // Below `sm` the kit Dialog's own `calc(100vw - 3rem)` width applies (mobile gutter);
+ // `max-w-none` lifts its 30rem cap so the dialog spans the full width minus the gutter.
+ expect(dialog.className).toContain("max-w-none");
+ expect(dialog.className).toContain("sm:w-[70vw]");
+ expect(dialog.className).toContain("sm:max-w-[70vw]");
+ expect(dialog.className).not.toContain("max-w-md");
+ });
+
+ it("uses 70vw for every non-sm size", () => {
+ for (const size of ["lg", "xl", "full"] as const) {
+ const dialog = renderModal({ size });
+ expect(dialog.className, size).toContain("sm:w-[70vw]");
+ expect(dialog.className, size).toContain("sm:max-w-[70vw]");
+ expect(dialog.className, size).not.toMatch(/(^|\s)max-w-(md|lg|xl|4xl)(\s|$)/);
+ cleanups.pop()!();
+ }
+ });
+
+ it("keeps the sm (confirm/alert) dialog narrow", () => {
+ const dialog = renderModal({ size: "sm" });
+ expect(dialog.className).toContain("max-w-sm");
+ expect(dialog.className).not.toContain("70vw");
+ });
+});