Skip to content

FreeIn reviewv1.0.0

Glass Dialog

A confirmation dialog on glass over a dimmed scrim, with focus kept inside and Escape to cancel.
npx shadcn@latest add @vibebox/glass-dialog

Controls

Tone

About

A confirm step that matches a glass interface. It does not manage open state or inert the page: render it when you need it and place it in a relative or fixed parent.

Use it when

  • Destructive or irreversible confirmations.

Skip it when

  • Long forms: use a page or a sheet.

Install

  1. 1Add the VibeBox registry to components.json:
    {
      "registries": {
        "@vibebox": "https://motion.vibeboxph.com/r/{name}.json"
      }
    }
  2. 2Add the component with the shadcn CLI:
    npx shadcn@latest add @vibebox/glass-dialog

AI prompt

Everything an AI coding tool needs to rebuild this component the way it looks here: colours, fonts, the art it draws, props and rules.

Read the prompt
Build the VibeBox Motion component "Glass Dialog" (Liquid glass) in a Next.js App Router project with Tailwind CSS v4.

What it is
- A confirmation dialog on glass over a dimmed scrim, with focus kept inside and Escape to cancel.
- An alert dialog on liquid glass: focus lands on the safe action, Tab stays inside, Escape cancels, and a scrim dims the page.

Install
- `npx shadcn@latest add @vibebox/glass-dialog`
- Registry: add `"@vibebox": "https://motion.vibeboxph.com/r/{name}.json"` to components.json, then run the command. The source lands in `components/vibebox/glass-dialog` and is yours to edit.

Visual spec
- Colours (the VibeBox spectrum, the same in light and dark):
    - --spectrum-1: #00c5fc (cyan)
    - --spectrum-2: #1d6ef0 (blue)
    - --spectrum-3: #5225ed (indigo)
    - --spectrum-4: #e434dc (magenta)
    - --spectrum-5: #fd3aa9 (pink)
    - --spectrum-6: #f98472 (coral)
    - --spectrum-7: #fcbe72 (amber)
- Ink surface (dark): background #05070c, card #0a0d15, text #f5f6fa, muted text #a3abbd, border rgba(255,255,255,0.08).
- Paper surface (light): background #f3f0ea, card #fbfaf7, text #0c0e14, muted text #4a4f5c, border rgba(12,14,20,0.12).
- Fonts:
    - Display and body: Figtree (400, 500, 600, 700), falling back to ui-sans-serif, system-ui.
    - Emphasis: Fraunces italic, for one or two words of a heading.
    - Labels and code: JetBrains Mono (400, 500), uppercase labels with 0.14em tracking.
- Shape: radius 8 to 12px on cards and panels; pills may be fully round. Hairline borders, soft glow instead of heavy shadow.
- Motion: animate transform and opacity only; fast ease-out (cubic-bezier(0.16, 1, 0.3, 1)), 140ms for micro-interactions, 320 to 520ms for reveals.

Art and media
- Built from HTML, CSS and inline SVG only; no image files or video.

Props
- `title`: `string`. The question.
- `children`: `ReactNode`. The explanation.
- `confirmLabel`: `string` (default "Delete"). Confirm button text.
- `cancelLabel`: `string` (default "Keep it"). Cancel button text.
- `danger`: `boolean` (default true). Red confirm button.
- `onConfirm`: `() => void`. Confirm handler.
- `onCancel`: `() => void`. Cancel and Escape handler.
- `scrim`: `boolean` (default true). Fill the positioned parent with a dim scrim and centre the dialog.
- `tone`: `"auto" | "dark" | "light"` (default "auto"). Tints the glass and sets the text colour for the page behind it.
- `className`: `string`. Merged with cn(); sets the width and margins.

Behaviour and access
- Reduced motion: The pointer sheen and sliding are off; state changes are instant.
- Keyboard: Focus starts on the cancel button; Tab and Shift+Tab stay inside; Escape cancels.
- Screen readers: role=alertdialog, aria-modal, labelled by its title.
- Touch: Buttons are 36 px tall.
- Budget: about 3 KB of JavaScript, WebGL none, GPU low. It pauses while off screen.

Theming
The surface reads `--g-tint`, `--g-edge`, `--g-ink`, `--g-blur` and `--g-radius` from its root; the demo backdrop uses `--spectrum-*`.

Notes
Add the VibeBox Motion Glass Dialog with `npx shadcn@latest add @vibebox/glass-dialog`. Render it only when open, inside a fixed full-screen wrapper, with a `title`, an explanation, and `onConfirm` and `onCancel`. Keep focus on the safe action, the focus trap and Escape handling.

Rules
- Keep the licence header on every installed file.
- Original code only: do not copy components from other libraries, and do not add GSAP.
- Use tokens for colour, type and radius; no hard-coded brand values.
- Server Components by default; `"use client"` only for the island.

Code

2 files
components/vibebox/glass-dialog/glass-dialog.client.tsx
/**
 * VibeBox Motion · Glass Dialog
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
"use client";

import { useEffect, useRef, type KeyboardEvent, type ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Glass } from "@/lib/vibebox/glass";

export type GlassDialogProps = {
  title?: string;
  children?: ReactNode;
  confirmLabel?: string;
  cancelLabel?: string;
  /** Marks the confirm action as destructive (a red face). */
  danger?: boolean;
  onConfirm?: () => void;
  onCancel?: () => void;
  /** Draw a dimming scrim that fills the nearest positioned parent (put the dialog in a `relative` box, or `fixed` for the page). */
  scrim?: boolean;
  tone?: "auto" | "dark" | "light";
  className?: string;
};

/**
 * A confirmation dialog on glass. Focus lands on the safe action, Escape cancels, and Tab stays
 * inside it. With `scrim` it fills its positioned parent with a dim backdrop and centres itself.
 */
export function GlassDialog({
  title = "Delete this project?",
  children = "This removes the project and its preview. You can restore it from the archive for 30 days.",
  confirmLabel = "Delete",
  cancelLabel = "Keep it",
  danger = true,
  onConfirm,
  onCancel,
  scrim = true,
  tone,
  className,
}: GlassDialogProps) {
  const root = useRef<HTMLDivElement>(null);
  const cancel = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    cancel.current?.focus({ preventScroll: true });
  }, []);

  const onKey = (event: KeyboardEvent) => {
    if (event.key === "Escape") {
      onCancel?.();
      return;
    }
    if (event.key !== "Tab" || !root.current) return;
    const stops = [...root.current.querySelectorAll<HTMLElement>("button")];
    const first = stops[0];
    const last = stops[stops.length - 1];
    if (event.shiftKey && document.activeElement === first) {
      last?.focus();
      event.preventDefault();
    } else if (!event.shiftKey && document.activeElement === last) {
      first?.focus();
      event.preventDefault();
    }
  };

  const dialog = (
    <Glass
      as="div"
      role="alertdialog"
      aria-modal="true"
      aria-labelledby="vb-gd-title"
      data-vb="glass-dialog"
      tone={tone}
      blur={22}
      onKeyDown={onKey}
      className={cn("w-full max-w-[22rem] p-5", className)}
    >
      <div ref={root}>
        <h2 id="vb-gd-title" className="text-[1.05rem] leading-snug font-semibold tracking-tight">
          {title}
        </h2>
        <p className="mt-1.5 text-[0.88rem] leading-relaxed opacity-80">{children}</p>
        <div className="mt-5 flex justify-end gap-2">
          <button
            ref={cancel}
            type="button"
            onClick={onCancel}
            className="h-9 rounded-lg border border-white/30 bg-white/15 px-3.5 text-[0.86rem] font-medium transition-colors hover:bg-white/25 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
          >
            {cancelLabel}
          </button>
          <button
            type="button"
            onClick={onConfirm}
            className={cn(
              "h-9 rounded-lg px-3.5 text-[0.86rem] font-semibold transition-[filter] hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
              danger ? "bg-[#ff4d6d] text-white" : "bg-white text-[#101521]",
            )}
          >
            {confirmLabel}
          </button>
        </div>
      </div>
    </Glass>
  );

  if (!scrim) return dialog;
  return <div className="absolute inset-0 z-20 grid place-items-center bg-black/30 p-4 backdrop-blur-[2px]">{dialog}</div>;
}
components/vibebox/glass-dialog/index.tsx
/**
 * VibeBox Motion · Glass Dialog
 * Copyright (c) 2026 VibeBox. Licensed under the VibeBox Motion Free License:
 * https://motion.vibeboxph.com/license
 * Use it in unlimited personal, commercial and client projects.
 * Do not sell or redistribute it as a component, kit, template or builder.
 */
export * from "./glass-dialog.client";

Props

PropTypeDefaultDescription
titlestringThe question.
childrenReactNodeThe explanation.
confirmLabelstring"Delete"Confirm button text.
cancelLabelstring"Keep it"Cancel button text.
dangerbooleantrueRed confirm button.
onConfirm() => voidConfirm handler.
onCancel() => voidCancel and Escape handler.
scrimbooleantrueFill the positioned parent with a dim scrim and centre the dialog.
tone"auto" | "dark" | "light""auto"Tints the glass and sets the text colour for the page behind it.
classNamestring—Merged with cn(); sets the width and margins.

Accessibility and motion

Keyboard
Focus starts on the cancel button; Tab and Shift+Tab stay inside; Escape cancels.
Screen readers
role=alertdialog, aria-modal, labelled by its title.
Touch
Buttons are 36 px tall.
Reduced motion
The pointer sheen and sliding are off; state changes are instant.
  • A glass control centre with four toggle tiles and two sliders

    A control-centre panel: glass toggle tiles and sliders that are real range inputs.

    Liquid glass · CSS

  • A row of glass app icons, the middle one enlarged, over a gradient

    A floating dock of glass icons that swell toward the pointer, with labels and badges.

    Liquid glass · CSS

  • A glass pill header with a wordmark, four links and a white call to action

    A floating pill header on glass with a highlight that glides between links.

    Liquid glass · CSS

↑ ↓ to move↵ to openesc to close