Component

Dialog

A modal framed in accent ticks, over an ink scrim.

stablev0.3.0 Chromium 103 ui-scale safe

Install

Copies the file into components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.
bunx shadcn@3 add @fastrp/dialog

Examples

Default

Open alone

API

<Dialog />Takes the props of what it wraps

Radix's dialog, sounding `open` and `close` however it opens or closes.

<DialogContent />

A menu window: a game panel landing in from slightly small and out of focus, an accent line along its top edge. Centred by auto margins rather than a translate, which the entrance's own transform would overwrite mid-animation.

PropTypeDefaultNotes
showClosebooleantrueThe ESC ✕ in the corner. On by default; turn it off when the footer has its own way out.
<DialogOverlay />Takes the props of what it wraps

The scrim: the world behind goes dark and out of focus, as a game pauses into a menu. The FiveM NUI turns backdrop-filter off, and the ink alone still separates the menu from the game.

Dependencies

npm

  • @radix-ui/react-dialog@^1.1.4
  • lucide-react@^0.469.0

Source

components/ds/dialog.tsxShow
'use client';

import * as DialogPrimitive from '@radix-ui/react-dialog';
import { X } from 'lucide-react';
import type * as React from 'react';
import { Kbd } from '@/components/ds/key-hint';
import { useSoundedOpen } from '@/components/ds/ui-sounds';
import { cn } from '@/lib/utils';

// @fast-ds [email protected]

/**
 * Radix's dialog, sounding `open` and `close` however it opens or closes.
 */
function Dialog({
  open,
  defaultOpen,
  onOpenChange,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
  return (
    <DialogPrimitive.Root {...props} {...useSoundedOpen({ open, defaultOpen, onOpenChange })} />
  );
}
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close;

/**
 * The scrim: the world behind goes dark and out of focus, as a game pauses into a menu. The
 * FiveM NUI turns backdrop-filter off, and the ink alone still separates the menu from the game.
 */
function DialogOverlay({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
  return (
    <DialogPrimitive.Overlay
      data-slot="dialog-overlay"
      className={cn(
        'fixed inset-0 z-overlay bg-ink/70 backdrop-blur-md data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
        className,
      )}
      {...props}
    />
  );
}

export type DialogContentProps = React.ComponentProps<typeof DialogPrimitive.Content> & {
  /** The ESC ✕ in the corner. On by default; turn it off when the footer has its own way out. */
  showClose?: boolean;
};

/**
 * A menu window: a game panel landing in from slightly small and out of focus, an accent line
 * along its top edge. Centred by auto margins rather than a translate, which the entrance's own
 * transform would overwrite mid-animation.
 */
function DialogContent({ className, children, showClose = true, ...props }: DialogContentProps) {
  return (
    <DialogPortal>
      <DialogOverlay />
      <DialogPrimitive.Content
        data-slot="dialog-content"
        className={cn(
          'panel fixed inset-0 z-modal m-auto grid h-fit max-h-[calc(100vh-2rem)] w-[calc(100%-2rem)] max-w-lg gap-5 overflow-y-auto overflow-x-hidden p-6 focus:outline-none data-[state=closed]:animate-menu-out data-[state=open]:animate-menu-in',
          className,
        )}
        {...props}
      >
        <span
          aria-hidden
          className="pointer-events-none absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary to-transparent"
        />
        {children}
        {showClose && (
          <DialogPrimitive.Close
            data-slot="dialog-close"
            className="absolute right-3 top-3 flex items-center gap-1.5 rounded-md px-1.5 py-1 text-muted-foreground transition-colors duration-fast hover:bg-foreground/[0.08] hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-selection [&_svg]:size-4"
          >
            <Kbd size="sm" aria-hidden>
              ESC
            </Kbd>
            <X />
            <span className="sr-only">Close</span>
          </DialogPrimitive.Close>
        )}
      </DialogPrimitive.Content>
    </DialogPortal>
  );
}

function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="dialog-header"
      className={cn('flex flex-col gap-2 pr-16 text-left', className)}
      {...props}
    />
  );
}

function DialogFooter({ className, ...props }: React.ComponentProps<'div'>) {
  return (
    <div
      data-slot="dialog-footer"
      className={cn(
        '-mx-6 -mb-6 mt-1 flex flex-col-reverse gap-2 border-t border-foreground/10 bg-black/15 px-6 py-4 sm:flex-row sm:items-center sm:justify-end',
        className,
      )}
      {...props}
    />
  );
}

function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
  return (
    <DialogPrimitive.Title
      data-slot="dialog-title"
      className={cn('font-display text-3xl leading-none text-foreground', className)}
      {...props}
    />
  );
}

function DialogDescription({
  className,
  ...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
  return (
    <DialogPrimitive.Description
      data-slot="dialog-description"
      className={cn('text-sm leading-relaxed text-muted-foreground', className)}
      {...props}
    />
  );
}

export {
  Dialog,
  DialogClose,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogOverlay,
  DialogPortal,
  DialogTitle,
  DialogTrigger,
};