GlassKit UI
Primitives
Copy for LLM

Grid

An aligned, vertically-scrolling multi-column layout: every cell shares the same track, so rows and columns line up. Drop any children in; it scrolls vertically and keeps a D-pad-focused child in view.

Apps · 10
Arrow the tiles · Enter opens

Installation

npx @glasskit-ui/cli add grid

Install the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:

npm install @glasskit-ui/react
// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string {  return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined {  return typeof label === "string" ? label : undefined;}
// components/glasskit/grid.tsx"use client";import { useLayoutEffect, useRef, type ReactNode } from "react";import { cn } from "../lib/utils";/** * <Grid> — an aligned, vertically-scrolling multi-column layout: every cell * shares the same track, so rows and columns line up. Drop any children in * (MediaThumb tiles, Pressable cards); the Grid scrolls vertically and keeps a * D-pad-focused child in view. Layout only: the children own their own * interactivity. */export function Grid({  columns = 2,  children,  className,}: {  /** Number of columns (2, 3, or 4). */  columns?: 2 | 3 | 4;  children: ReactNode;  className?: string;}) {  const scrollRef = useRef<HTMLDivElement>(null);  // Keep a D-pad-focused child scrolled into view (focusin bubbles up).  useLayoutEffect(() => {    const el = scrollRef.current;    if (!el) return;    const onFocusIn = (e: FocusEvent) => {      const t = e.target as HTMLElement | null;      if (t && el.contains(t) && typeof t.scrollIntoView === "function") {        t.scrollIntoView({ block: "nearest", behavior: "smooth" });      }    };    el.addEventListener("focusin", onFocusIn);    return () => el.removeEventListener("focusin", onFocusIn);  }, []);  return (    <div ref={scrollRef} data-cols={columns} className={cn("gk-grid", className)}>      {children}    </div>  );}

Usage

<Grid columns={2}>  {photos.map((p) => (    <MediaThumb key={p.id} src={p.src} onSelect={() => open(p)} />  ))}</Grid>

Props

PropTypeDefaultDescription
columns2 | 3 | 42Number of equal columns.
childrenReactNodeThe cells (e.g. MediaThumb tiles or Pressable cards).

When to use

Reach for Grid when every cell is the same shape and you want them aligned in neat rows: a grid of square thumbnails, a launcher-like set of equal cards. Pass columns (2, 3, or 4) and it lays them out on equal tracks, scrolling vertically.

Grid is layout only: the children own their interactivity (a MediaThumb with onSelect, or a Pressable).

On this page