Centered Screen

A full-viewport centering wrapper. Renders a <section> that pins its children to the optical centre of the screen using a flex column with a configurable minimum height — useful for empty states, splash screens, sign-in surfaces, 404 pages, and terminal-style "you have arrived" routes. The wrapper owns vertical centering, horizontal centering, a max-width clamp, a padded gutter, and a subtle opacity fade-in on mount. It does not own the content — drop any node.

Welcome back

Pick up where you left off — your last session is still warm and a fresh stretch of cards is ready when you are.

Installation

npx shadcn@latest add https://craftbits.dev/r/centered-screen.json

Usage

Wrap any column of content. The wrapper handles the gutter, the max-width clamp, and the vertical centering.

import { CenteredScreen, Button } from "@craft-bits/core";
 
<CenteredScreen minHeight="screen">
  <h1>Welcome back</h1>
  <p>Pick up where you left off.</p>
  <Button variant="primary">Resume session</Button>
</CenteredScreen>

Tune the horizontal clamp for narrower forms (sign-in / sign-up):

<CenteredScreen minHeight="screen" maxWidth="sm">
  <SignInForm />
</CenteredScreen>

Or drop the clamp entirely when the child owns its own width:

<CenteredScreen minHeight="tall" maxWidth="full">
  <Splash />
</CenteredScreen>

Understanding the component

  1. Landmark by default. Always renders a <section> so the wrapper is announced as a self-contained region. Pair with aria-label when the screen needs a name (e.g. "Sign in").
  2. Vertical span via prop. minHeight="screen" pins the content to the viewport centre (min-h-screen); tall reserves 60vh (the source-project default — sits inside a longer scroll page); auto disables the clamp so the wrapper sizes to its content.
  3. Horizontal clamp. The content column is centred via mx-auto and clamped by maxWidth (default 2xl). Pass full to drop the clamp when the child already manages its own width.
  4. Stacked rhythm. Children stack as a flex column with gap-6 between siblings. The gutter is px-6 so content never bleeds against a narrow viewport edge.
  5. Subtle fade on mount. The wrapper fades in from opacity: 0 → 1 via SPRINGS.smooth. Honours prefers-reduced-motion — the static surface renders immediately when the user opts out. Pass disableMotion to skip the fade when nested inside an already-animated container.
  6. Data-attributes for nested styling. The root carries data-cb-centered-screen, data-min-height, and data-max-width so consumers can target nested regions without re-deriving props.

Variants

Splash (full screen, narrow column)

<CenteredScreen minHeight="screen" maxWidth="sm">
  <Wordmark />
  <SignInForm />
</CenteredScreen>

Empty state inside a card body

<CenteredScreen minHeight="auto" maxWidth="md">
  <EmptyIcon />
  <p>No items yet — add one to get started.</p>
</CenteredScreen>

Props

PropTypeDefaultDescription
childrenReactNodeThe content to centre. Stacks as a flex column with gap-6.
maxWidth'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full''2xl'Horizontal clamp for the content column. full disables the clamp.
minHeight'screen' | 'tall' | 'auto''tall'Vertical span. screen fills the viewport, tall reserves 60vh, auto disables.
disableMotionbooleanfalseSkip the opacity fade-in on mount. The wrapper still respects prefers-reduced-motion regardless.
classNamestringMerged onto the rendered <section>.

Accessibility

  • The root renders a <section> element. Inside a <main> or <article>, that means assistive tech announces the wrapper as a self-contained region. Pair with aria-label when the screen has a name (e.g. aria-label="Sign in").
  • No interactive affordances are added by the wrapper. Focus order flows through the children in source order.
  • Color contrast: the wrapper itself contributes no foreground — only the body's --cb-fg and any nested --cb-fg-muted text. Both pass WCAG AA against --cb-bg and --cb-bg-elevated.
  • The mount fade respects prefers-reduced-motion — the wrapper renders the static surface immediately when the user has reduced-motion on.
  • The fade is opacity-only (no transform, no displacement), so no layout-thrash or scroll jump at the moment of mount.

Credits

  • Extracted from: AlgoFlashcards (src/platform/ui/CenteredScreen.tsx). The original was a 20-line motion.div that took children + className and rendered a flex column with min-h-[60vh], max-w-2xl, and an inline SPRING.smooth fade-in. craft-bits keeps the surface intent intact and widens the API to maxWidth (7 stops including full), minHeight (screen / tall / auto), and disableMotion. The container is lifted from <div> to <section> for landmark semantics, the inline spring is rewired to SPRINGS.smooth from @craft-bits/core/motion, and the wrapper now honours prefers-reduced-motion explicitly.