Components

Scorekeeper Layout

The game-day app shell - pinned score header, scrolling work area, and a side panel that becomes a pull-up sheet on phones.

11:23
Live/Field 1
Game 24 / Synced
▼ 4of 5
Whiffleblowers
3
Bat Attitudes
At bat
4

The shell the Schott Classic scorekeeping app runs in: score pinned at the top, controls scrolling in the middle, play-by-play in a sidebar on wide screens and behind a pull-up sheet on phones. Rewritten dependency-free - no portal, no dialog library. The sheet positions inside the layout root rather than the viewport, so the component works full-screen (h-dvh) or embedded in a page, like the demo below.

Install#

npx shadcn@latest add https://benhof.ai/r/scorekeeper-layout.json
import { ScorekeeperLayout } from "@/components/scorekeeper-layout"

<ScorekeeperLayout
  className="h-dvh"
  header={<ScorePanel ... />}
  panel={<PlayByPlay events={events} />}
  footer={<GameControls />}
>
  <YourScoringControls />
</ScorekeeperLayout>

Props#

PropTypeDefaultDescription
headerReactNode-Pinned at the top - typically a score panel.
panelReactNode-Sidebar on md+, pull-up sheet below. Omit for a plain header + content shell.
panelTitlestring"Play-by-play"Labels the sidebar, the pull-up bar, and the sheet.
footerReactNode-Pinned action rail that shares space with the phone panel trigger.
forceMobilebooleanfalseKeeps the phone layout active inside a narrow preview on a wide page.
compactbooleanfalseReduces shell padding and control height for one-screen phone interfaces.
childrenReactNode-The scrolling work area.

Notes#

Give the root a height via className - it's a flex column that manages its own overflow. The sheet is a plain absolutely-positioned panel with a backdrop; it deliberately skips focus-trapping and body scroll-locking to stay dependency-free, so if you need full dialog semantics, swap in your app's sheet primitive around panel.