diff --git a/apps/web/index.html b/apps/web/index.html
index 5a4ccce76d6..afb4b2402a0 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -198,6 +198,9 @@
}
};
+ if (/^win(dows)?/i.test(navigator.platform)) {
+ document.documentElement.classList.add("windows");
+ }
try {
const storedTheme = window.localStorage.getItem("t3code:theme");
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
diff --git a/apps/web/package.json b/apps/web/package.json
index dfec330a107..b78171a28dd 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -20,6 +20,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@effect/atom-react": "catalog:",
+ "@fontsource-variable/inter": "^5.3.0",
"@formkit/auto-animate": "^0.9.0",
"@legendapp/list": "3.3.3",
"@lexical/react": "^0.41.0",
diff --git a/apps/web/src/appearanceFonts.test.ts b/apps/web/src/appearanceFonts.test.ts
index 31a2f1d779c..28cb7ecbd86 100644
--- a/apps/web/src/appearanceFonts.test.ts
+++ b/apps/web/src/appearanceFonts.test.ts
@@ -7,6 +7,7 @@ import {
clampPromptFontSize,
DEFAULT_CODE_FONT_STACK,
DEFAULT_SANS_FONT_STACK,
+ WINDOWS_SANS_FONT_STACK,
appearanceFontStack,
cssFontFamilies,
resolveDefaultFamilyLabel,
@@ -68,6 +69,12 @@ describe("appearanceFontStack", () => {
it("falls back to the default stack when unset", () => {
expect(appearanceFontStack("", DEFAULT_SANS_FONT_STACK)).toBe(DEFAULT_SANS_FONT_STACK);
});
+
+ it("prepends to the Windows stack on Windows so Inter stays in the fallback chain", () => {
+ expect(appearanceFontStack("Cascadia Code", WINDOWS_SANS_FONT_STACK)).toBe(
+ `"Cascadia Code", ${WINDOWS_SANS_FONT_STACK}`,
+ );
+ });
});
describe("resolveTerminalFontPreference", () => {
diff --git a/apps/web/src/appearanceFonts.ts b/apps/web/src/appearanceFonts.ts
index 6053e5fb0dd..cb6722c3346 100644
--- a/apps/web/src/appearanceFonts.ts
+++ b/apps/web/src/appearanceFonts.ts
@@ -20,6 +20,9 @@ import {
export const DEFAULT_SANS_FONT_STACK =
'-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
+export const WINDOWS_SANS_FONT_STACK =
+ '"Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif';
+
// Concrete names first: some engines alias `ui-monospace` to the
// proportional system UI font, which would break every code surface.
export const DEFAULT_CODE_FONT_STACK =
@@ -100,8 +103,15 @@ export function applyAppearanceFontVariables(
root: HTMLElement,
preferences: AppearanceFontPreferences,
): void {
+ const windowsSans = root.classList.contains("windows");
const families: ReadonlyArray = [
- ["--font-sans", preferences.sans, DEFAULT_SANS_FONT_STACK],
+ // On Windows the stylesheet default already prefers Inter, so custom
+ // families must prepend to the same stack or glyph fallbacks skip it.
+ [
+ "--font-sans",
+ preferences.sans,
+ windowsSans ? WINDOWS_SANS_FONT_STACK : DEFAULT_SANS_FONT_STACK,
+ ],
["--font-mono", preferences.code, DEFAULT_CODE_FONT_STACK],
// The composer falls back to whatever the sans preference resolves to.
["--font-composer", preferences.composer, "var(--font-sans)"],
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 6743952ae26..6f0380d61fe 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -75,7 +75,7 @@ import {
sortProviderInstanceEntries,
} from "../../providerInstances";
import { ensureLocalApi, readLocalApi } from "../../localApi";
-import { isMacPlatform } from "../../lib/utils";
+import { isMacPlatform, isWindowsPlatform } from "../../lib/utils";
import { primaryServerObservabilityAtom, primaryServerProvidersAtom } from "../../state/server";
import { useProjects } from "../../state/entities";
import { useArchivedThreadSnapshots } from "../../lib/archivedThreadsState";
@@ -96,6 +96,7 @@ import { Input } from "../ui/input";
import {
DEFAULT_CODE_FONT_STACK,
DEFAULT_SANS_FONT_STACK,
+ WINDOWS_SANS_FONT_STACK,
isFontFamilyAvailable,
isMonospaceFamily,
resolveDefaultFamilyLabel,
@@ -1078,15 +1079,19 @@ export function AppearanceSettingsPanel() {
function useFontDefaultFamilies() {
const settings = usePrimarySettings();
- // An unset preference shows the font it resolves to on this machine; the
- // default stacks are the platform's own faces, so the name is probed, not
- // hardcoded.
+ const sansStack = isWindowsPlatform(navigator.platform)
+ ? WINDOWS_SANS_FONT_STACK
+ : DEFAULT_SANS_FONT_STACK;
const defaults = useMemo(
() => ({
- sans: resolveDefaultFamilyLabel(DEFAULT_SANS_FONT_STACK) ?? "System default",
+ // The canvas probe cannot measure the bundled Inter webfont before it
+ // renders, so on Windows fall back to naming it directly.
+ sans:
+ resolveDefaultFamilyLabel(sansStack) ??
+ (isWindowsPlatform(navigator.platform) ? "Inter Variable" : "System default"),
code: resolveDefaultFamilyLabel(DEFAULT_CODE_FONT_STACK) ?? "System monospace",
}),
- [],
+ [sansStack],
);
return {
sans: defaults.sans,
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index 8600d004854..84e16686d01 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -1442,6 +1442,11 @@ body {
--desktop-window-right-resize-inset: 6px;
}
+html.windows {
+ --font-sans:
+ "Inter Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
+}
+
/* App-chrome grain. Baked into each surface's own background (behind
content) rather than a fixed overlay on top: a full-viewport overlay
forces the compositor to re-blend every frame any animation produces,
diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx
index c8f6f4de97b..9b510716978 100644
--- a/apps/web/src/main.tsx
+++ b/apps/web/src/main.tsx
@@ -7,6 +7,7 @@ import { createHashHistory, createBrowserHistory } from "@tanstack/react-router"
import "./index.css";
+import "@fontsource-variable/inter/wght.css";
import { isElectron } from "./env";
import { ManagedRelayAuthProvider } from "./cloud/managedAuth";
import { hasCloudPublicConfig } from "./cloud/publicConfig";
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9b999993183..e786d7588f7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -533,6 +533,9 @@ importers:
'@effect/atom-react':
specifier: 4.0.0-beta.103
version: 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=af36b7948b6f9c56623074662b51dade5699880c1a7c71245de73e13c3185fb6))(react@19.2.6)(scheduler@0.27.0)
+ '@fontsource-variable/inter':
+ specifier: ^5.3.0
+ version: 5.3.0
'@formkit/auto-animate':
specifier: ^0.9.0
version: 0.9.0
@@ -2708,6 +2711,9 @@ packages:
'@floating-ui/utils@0.2.11':
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
+ '@fontsource-variable/inter@5.3.0':
+ resolution: {integrity: sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==}
+
'@formkit/auto-animate@0.9.0':
resolution: {integrity: sha512-VhP4zEAacXS3dfTpJpJ88QdLqMTcabMg0jwpOSxZ/VzfQVfl3GkZSCZThhGC5uhq/TxPHPzW0dzr4H9Bb1OgKA==}
@@ -12805,6 +12811,8 @@ snapshots:
'@floating-ui/utils@0.2.11': {}
+ '@fontsource-variable/inter@5.3.0': {}
+
'@formkit/auto-animate@0.9.0': {}
'@hono/node-server@1.19.14(hono@4.12.27)':