skills.vishalvoid
all skills
Design

Grid Layout System

A structured CSS Grid design system for building polished app shells with zero layout drift. Implements the exact layout patterns used in Linear, Vercel, and Resend — sidebar rails, content panels, detail drawers, and responsive breakpoints — as a composable, predictable system.

CSS GridTailwind CSSDesign SystemLayout

Install

$curl -fsSL https://skills.vishalvoid.com/install grid-layout-system | bash

The two primitives

Every professional app shell is built from two CSS Grid primitives: the shell (full-viewport grid that divides space between navigation and content) and the content area (inner grid that composes panels, lists, and detail views). Everything else is composition of these two.

The app shell

A `48px` top bar spans the full width. Below it, the sidebar rail (`240px` fixed) and content area (`1fr`) divide the remaining space. The grid never overflows because nothing inside it uses `position: absolute` for layout.

app-shell.css
/* app-shell.css */
.app-shell {
  display: grid;
  grid-template-columns: 240px 1fr;
  grid-template-rows: 48px 1fr;
  height: 100dvh;        /* dvh: accounts for mobile browser chrome */
  overflow: hidden;
}

.app-shell__topbar {
  grid-column: 1 / -1;  /* spans full width */
  grid-row: 1;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  padding: 0 16px;
}

.app-shell__sidebar {
  grid-column: 1;
  grid-row: 2;
  overflow-y: auto;
  border-right: 1px solid var(--border);
}

.app-shell__content {
  grid-column: 2;
  grid-row: 2;
  overflow-y: auto;      /* content scrolls, shell never does */
}

Tailwind CSS implementation

The same shell in Tailwind — no custom CSS needed. Grid rows and columns are defined inline. The key insight: `h-dvh overflow-hidden` on the shell, `overflow-y-auto` on the scrollable regions.

components/AppShell.tsx
// components/AppShell.tsx
export function AppShell({ children }: { children: React.ReactNode }) {
  return (
    <div className="grid h-dvh overflow-hidden"
         style={{ gridTemplateColumns: '240px 1fr', gridTemplateRows: '48px 1fr' }}>
      {children}
    </div>
  )
}

export function Topbar({ children }: { children: React.ReactNode }) {
  return (
    <header className="col-span-full row-start-1 flex items-center px-4
                       border-b border-[#1f1f1f]">
      {children}
    </header>
  )
}

export function Sidebar({ children }: { children: React.ReactNode }) {
  return (
    <aside className="row-start-2 col-start-1 overflow-y-auto
                      border-r border-[#1f1f1f]">
      {children}
    </aside>
  )
}

export function Content({ children }: { children: React.ReactNode }) {
  return (
    <main className="row-start-2 col-start-2 overflow-y-auto">
      {children}
    </main>
  )
}

Responsive sidebar collapse

On mobile, the sidebar rail collapses to `0px` via a CSS variable. A `data-open` attribute toggles the sidebar into view as a fixed overlay. The layout grid handles the reflow automatically — the content expands to fill the full width.

app-shell.css
/* Mobile-first sidebar collapse */

.app-shell {
  --sidebar-w: 240px;
  grid-template-columns: var(--sidebar-w) 1fr;
}

@media (max-width: 768px) {
  .app-shell {
    --sidebar-w: 0px;
    grid-template-columns: var(--sidebar-w) 1fr;
  }

  .app-shell__sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: 240px;
    z-index: 50;
    transform: translateX(-100%);
    transition: transform 220ms cubic-bezier(0.4, 0, 0.2, 1);
    background: var(--bg);
    border-right: 1px solid var(--border);
  }

  .app-shell__sidebar[data-open='true'] {
    transform: translateX(0);
    box-shadow: 4px 0 24px rgba(0, 0, 0, 0.4);
  }
}

Detail panel (the Linear pattern)

When a row is selected, a third column appears to the right. The grid adds a `380px` column via `data-panel`. No absolute positioning — the content area narrows automatically as the panel opens.

app-shell.css
/* Panel system */
.app-shell[data-panel] {
  grid-template-columns: 240px 1fr 380px;
}

.app-shell__panel {
  display: none;
  grid-row: 2;
  grid-column: 3;
  overflow-y: auto;
  border-left: 1px solid var(--border);
  animation: slideInRight 160ms ease;
}

.app-shell[data-panel] .app-shell__panel {
  display: block;
}

@keyframes slideInRight {
  from { opacity: 0; transform: translateX(8px); }
  to   { opacity: 1; transform: translateX(0); }
}

/* Collapse panel on mobile — stack vertically instead */
@media (max-width: 1024px) {
  .app-shell[data-panel] {
    grid-template-columns: 0px 1fr;
    grid-template-rows: 48px 1fr 1fr;
  }

  .app-shell[data-panel] .app-shell__panel {
    grid-column: 2;
    grid-row: 3;
    border-left: none;
    border-top: 1px solid var(--border);
  }
}

Content area grid

Inside the content area, use another grid for list-detail or card compositions. This nested grid is independent — it responds to the content area's width, not the full viewport.

content-grid.css
/* Content area: list + detail side by side */
.content-grid {
  display: grid;
  grid-template-columns: 340px 1fr;
  height: 100%;
}

/* Collapse to single column on narrow viewports */
@media (max-width: 900px) {
  .content-grid {
    grid-template-columns: 1fr;
  }

  .content-grid__detail {
    display: none;
  }

  .content-grid[data-selected] .content-grid__list {
    display: none;
  }

  .content-grid[data-selected] .content-grid__detail {
    display: block;
  }
}

Related skills

back to all skills