GlassKit UI
Navigation
Copy for LLM

Launcher

The app grid: the entry screen for a multi-app surface. Two columns of D-pad-focusable cards on gradient icon plates. Keep it to about six apps so the whole grid is one glance.

Pick an app

Installation

npx @glasskit-ui/cli add launcher

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/launcher.tsximport type { ReactNode } from "react";import { cn } from "../lib/utils";/** Tasteful gradient tones for the app plates (see styles.css `.gk-grad-*`). */export type LauncherTone =  | "blue"  | "green"  | "peach"  | "violet"  | "cyan"  | "amber";const TONES: LauncherTone[] = [  "blue",  "green",  "peach",  "violet",  "cyan",  "amber",];export type LauncherApp = {  id: string;  label: ReactNode;  tagline?: ReactNode;  /** Optional glyph — a stroke SVG (rendered white on the gradient plate). */  icon?: ReactNode;  /** Gradient tone for the icon plate; defaults to a cycled palette color. */  tone?: LauncherTone;  onSelect?: () => void;};/** * <Launcher> — the app grid: a home screen of gradient app-icon plates + labels. * Cards are D-pad-focusable (useDpad walks them, Enter activates); focus lifts * the plate. Two columns; keep it to ~6 apps so the grid is one glance. RTL-safe. */export function Launcher({  apps,  className,}: {  apps: LauncherApp[];  className?: string;}) {  return (    <div className={cn("grid w-full grid-cols-2 gap-4", className)}>      {apps.map((a, i) => (        <button          key={a.id}          type="button"          onClick={a.onSelect}          className="focusable gk-launcher-card press-scale flex flex-col items-center gap-[9px] rounded-[22px] border-transparent px-[10px] py-[14px] text-center"        >          {a.icon != null ? (            <span              className={cn(                "gk-launcher-card__icon gk-plate mb-0.5 size-[94px] [&_.gk-icon]:size-[46px]",                `gk-grad-${a.tone ?? TONES[i % TONES.length]}`,              )}            >              {a.icon}            </span>          ) : null}          <span className="gk-launcher-card__label t-body font-semibold">            {a.label}          </span>          {a.tagline != null ? (            <span className="t-caption text-foreground-faint">{a.tagline}</span>          ) : null}        </button>      ))}    </div>  );}

Usage

<Launcher apps={[  { id: "nav", label: "Navigate", tagline: "320 m",    icon: <Icon active><NavIcon /></Icon>, onSelect: openNav },  { id: "msg", label: "Messages", tagline: "2 new",    icon: <Icon><MessageIcon /></Icon>, onSelect: openMessages },]} />

Props

PropTypeDefaultDescription
apps{ id, label, tagline?, icon?, tone?, onSelect? }[]The apps to show as focusable cards. Each card may set a gradient tone; it defaults to a cycled palette color.

How to use it

Launcher is the app's front door: a grid of destinations, each on a tasteful gradient icon plate. A tile opens one of the other models (a Navigator stack, a Tabs shell, a Deck flow). Keep it to about six apps so the grid is one glance, two columns, no scrolling.

A grid of entries

Each entry is a focusable plate with an icon, a label, and an onSelect handler. The gradient tone defaults to a cycled palette color, so a launcher looks deliberate without you choosing every color.

import { Launcher } from "@/components/ui/launcher";

<Launcher
  apps={[
    { icon: <IconRun />, label: "Workout", onSelect: () => open("workout") },
    { icon: <IconMap />, label: "Navigate", tone: "cyan", onSelect: () => open("nav") },
    { icon: <IconMusic />, label: "Music", onSelect: () => open("music") },
    { icon: <IconBell />, label: "Alerts", onSelect: () => open("alerts") },
  ]}
/>;

The grid is RTL-safe and D-pad navigable: the focus ring walks the plates and Enter fires onSelect.

Launcher and the back gesture

A Launcher is usually the root, so the back gesture from it falls through to the system (the app switcher), the same as any root screen. When a tile opens a Navigator, pushes from there put history entries on the stack and back returns through them until it reaches the Launcher again.

On this page