Skip to content

Framing an application

Frame gives your application its layout: a navigation column on the left, called the rail, and a content area beside it, called the canvas.

<Frame.Root location={pathname}>
<Frame.Rail brand={<HomeLink />}>
<YourNavigation />
</Frame.Rail>
<Frame.Canvas canvas={mode}>
<YourScreen />
</Frame.Canvas>
</Frame.Root>

A region exists because you render its element. There is no showRail flag, so an application without navigation simply does not render a Frame.Rail.

Every component’s props interface is exported under its own name, FrameRootProps, FrameRailProps and FrameCanvasProps, for when you wrap one in a component of your own.

On a wide screen the canvas is a panel 16px from the top, right and bottom of the window. On the rail side it has no margin of its own, because the rail’s 16px padding already parts the two, so the gap from the rail’s highlighted item to the panel is 16px as well. The WordPress Site Editor frames its canvas the same way. When no rail is on screen, the panel keeps its 16px on that side too.

  • Below 1024px the rail goes away. In its place you get a top bar, and its menu button opens your same navigation in a drawer that slides in.
  • Below 782px the canvas stretches to the screen edges, with no margin, no rounded corners and no shadow, the way a WordPress admin page meets the screen at that width.
  • Below 640px the canvas also uses tighter padding, 16px instead of 24px on each side.

The top bar is 46px tall, like the WordPress admin bar on a phone. Its menu button is drawn like the admin bar menu toggle: three bars, 28px wide and 4px thick, in a 52px wide button at the start of the bar. The bars take the text colour of the chrome theme at 60%, so they follow your chrome colour and light up under the pointer. The admin bar draws its icons in a fixed bluish white at 60%, which looks almost the same on a dark bar.

You write the navigation once. The rail and the drawer render the same Frame.Rail children.

Two Frame.Rail props exist for that top bar:

  • brand is a small element such as a home link, shown beside the menu button. The top bar only exists on a small screen, so brand is not rendered at all on a wide one. Do not make it a heading: each screen owns the single h1 on the page, and Page renders it.
  • menuLabel is the accessible name of the menu button and of the drawer it opens. It defaults to Open navigation, so set it when your application speaks another language.

The widths are exported, so your own rules can change at the same points:

Export Value
RAIL_BREAKPOINT 1024
EDGE_BREAKPOINT 782
DENSE_BREAKPOINT 640
SMALL_VIEWPORT (max-width: 1023px)

useMediaQuery answers any media query and re-renders your component when the answer changes:

const small = useMediaQuery(SMALL_VIEWPORT)

Frame.Root takes location as a plain string, and the only thing it does with it is close the drawer when the string changes. That is enough: click any link, the URL changes, the drawer closes. It works for links a plugin added too, because nothing has to be registered. This is also why the main entry point imports no router.

chromeColor sets the theme color of the frame, and canvasColor the one of the canvas. Each accepts the same color values as the design system theme provider. Pass neither and the frame looks like WordPress: the chrome gets { background: '#26292b' } and the canvas { background: '#fcfcfc' }. Up to godmin 0.15.0 the frame set no colour of its own, so an application that passes none sees its frame change.

The theme provider works out every grey from the background you give it. The frame paints its chrome with the weak surface of that theme, so #26292b draws the rail and the top bar in #1d2428, one step from the WordPress admin bar, #1d2327.

The canvas paints the strong surface of its theme, which is white, like a WordPress page. #fcfcfc gives the exact WordPress greys, row lines #f0f0f0 and muted text #707070. A white background moves every grey a step lighter.

Dialogs, drawers, menus, popovers and selects you open inside the frame do not take these colors. They use the AdminRoot color instead. Tooltips still follow the frame colors. The navigation drawer is an exception and keeps chromeColor.

Frame.Canvas takes canvas, typed as CanvasMode, with two values:

  • padded, the default, gives your screen comfortable padding.
  • bleed removes it, for a screen that manages its own edges, such as a full height table or a two pane chat view.

With TanStack Router, @gopherium/godmin/router lets each route declare the canvas it wants:

createRoute({ path: 'threads/$id', staticData: { canvas: 'bleed' } })

Read the declarations back with two hooks:

const mode = useCanvas() // hand this to Frame.Canvas
const pathname = useFrameLocation() // hand this to Frame.Root

When routes nest, the deepest match that declares a canvas wins, and a route that declares nothing inherits from the routes above it. So a section can declare bleed once and every screen inside it gets it, and one child can still declare padded to opt back out. Only when no matched route declares anything does useCanvas fall back to padded.

@tanstack/react-router is an optional peer dependency, needed only for this entry point. The main entry point never imports it, so an application on another router just passes location itself.