Foundations
Text size
A viewer can have a console drawn larger: Default, Large or Larger. The choice is saved in their browser under somnia-text-size, the same key in every Somnia app, so it carries across the apps and explorer zones on one origin, and it applies before the first paint. The whole page is zoomed, so type, spacing and borders keep their proportions and no app needs a larger type scale of its own.
Try it
A size picked here zooms this site, as it would your app; Default puts it back. The console demo is a full-height frame with the control in its top bar.
Sizes
The size is data-text-size on <html>, and no attribute is Default. The theme zooms the page by the size's factor and sets --ui-zoom to it. The components set their type in px, so a larger root font size would not move it; zoom does.
Set up
<ThemeBootstrap textSize /> applies the saved theme and text size before first paint, and puts them back on a page the browser draws from scratch, from one set of options. textSizeScript() is the text size's part alone, for an app with a theme script of its own, which then renders SavedPreferences with the same options itself. The control highlights the size in use from the attribute, in CSS, so the server and the browser draw the same markup.
import { ThemeBootstrap, Topbar } from "@somnia-chain/ui"
<html data-theme="light" suppressHydrationWarning>
<head>
{/* The saved theme and text size, before first paint and on a page the browser draws from scratch */}
<ThemeBootstrap textSize />
</head>
<body>{children}</body>
</html>
// The control: in the top bar, or anywhere else, such as a settings page
<Topbar textSize />
<TextSizeControl />Full-height frames
Zoom multiplies viewport units too. At Larger, 100dvh is a quarter taller than the window, so a frame of h-dvh scrolls, and w-screen runs off the side. Use the viewport utilities, not raw dvh or svh, for full-height frames: they divide by --ui-zoom, so they are the window's height at every size. AppShell, Sidebar, DocsShell and DocsSearch already use them.
The -s ones are svh: the window with a phone's browser bars shown, which stays put as they slide away. For any other viewport length, divide it by var(--ui-zoom,1) yourself. Percentages, fixed insets such as inset-0, and media queries need nothing.
// A full-height frame: the window's height at every size
<div className="flex h-viewport flex-col">…</div>
<aside className="sticky top-0 h-viewport-s">…</aside>
// Any other viewport length: divide it by --ui-zoom
<ul className="max-h-[calc(60vh/var(--ui-zoom,1))] overflow-y-auto">…</ul>
<div className="w-[min(420px,calc(100vw/var(--ui-zoom,1)-32px))]">…</div>In script and on a phone
getBoundingClientRect() and the other measurements come back in the window's pixels, which at Large and Larger are --ui-zoom times the CSS ones. Read the factor with getComputedStyle(document.documentElement).getPropertyValue("--ui-zoom") when you place something by hand. Media queries still read the window's pixels, so a 1024px window at Larger shows the sidebar beside the content, as at Default, with less room for the content; and a 390px phone at Larger has the room of a 312px window, so a top bar's controls have to fit in that.