Skip to content

Dashboard quick actions

The four most common tasks, as full-card links.

Applicationstarterdashboardactionsshortcutsoverview

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import Link from 'next/link'
import { ArrowRight, KeyRound, Rocket, UserPlus, Webhook } from 'lucide-react'
import { Panel } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

/**
 * Dashboard quick actions
 *
 * The four things a user does most, as a grid of full-card links. Placing them
 * on the overview removes two navigation steps from the most common tasks —
 * which is the only justification a quick-action panel ever has.
 */
const actions = [
  {
    icon: Rocket,
    title: 'Deploy',
    description: 'Ship the current branch to production.',
    href: '/starters/admin/preview',
    badge: 'Most used',
  },
  {
    icon: UserPlus,
    title: 'Invite a member',
    description: 'Send a join link with a role attached.',
    href: '/starters/admin/preview/users',
  },
  {
    icon: KeyRound,
    title: 'Create an API key',
    description: 'Scoped to one environment.',
    href: '/starters/admin/preview/settings',
  },
  {
    icon: Webhook,
    title: 'Add a webhook',
    description: 'With automatic retry and replay.',
    href: '/starters/admin/preview/settings',
  },
]

export default function DashboardQuickActions() {
  return (
    <section className="bg-canvas p-4 sm:p-6">
      <Panel title="Quick actions" description="The four most common tasks" headingLevel="h2">
        <ul className="grid gap-3 sm:grid-cols-2">
          {actions.map((action) => {
            const Icon = action.icon
            return (
              <li key={action.title}>
                <Link
                  href={action.href}
                  className="group flex h-full items-start gap-3 rounded-lg border border-line bg-surface p-4 transition-colors hover:border-accent"
                >
                  <span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-accent-soft text-accent-soft-ink">
                    <Icon className="size-4.5" aria-hidden="true" />
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="flex flex-wrap items-center gap-2">
                      <span className="text-sm font-semibold text-ink-strong group-hover:text-accent">
                        {action.title}
                      </span>
                      {action.badge ? <Badge size="sm">{action.badge}</Badge> : null}
                    </span>
                    <span className="mt-0.5 block text-xs text-ink-muted">
                      {action.description}
                    </span>
                  </span>
                  <ArrowRight
                    className="size-4 shrink-0 text-ink-subtle transition-transform group-hover:translate-x-0.5"
                    aria-hidden="true"
                  />
                </Link>
              </li>
            )
          })}
        </ul>
      </Panel>
    </section>
  )
}

components/blocks/sections/dashboard/quick-actions.tsx

import Link from 'next/link'
import { ArrowRight, KeyRound, Rocket, UserPlus, Webhook } from 'lucide-react'
import { Panel } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

/**
 * Dashboard quick actions
 *
 * The four things a user does most, as a grid of full-card links. Placing them
 * on the overview removes two navigation steps from the most common tasks —
 * which is the only justification a quick-action panel ever has.
 */
const actions = [
  {
    icon: Rocket,
    title: 'Deploy',
    description: 'Ship the current branch to production.',
    href: '/starters/admin/preview',
    badge: 'Most used',
  },
  {
    icon: UserPlus,
    title: 'Invite a member',
    description: 'Send a join link with a role attached.',
    href: '/starters/admin/preview/users',
  },
  {
    icon: KeyRound,
    title: 'Create an API key',
    description: 'Scoped to one environment.',
    href: '/starters/admin/preview/settings',
  },
  {
    icon: Webhook,
    title: 'Add a webhook',
    description: 'With automatic retry and replay.',
    href: '/starters/admin/preview/settings',
  },
]

export default function DashboardQuickActions() {
  return (
    <section className="bg-canvas p-4 sm:p-6">
      <Panel title="Quick actions" description="The four most common tasks" headingLevel="h2">
        <ul className="grid gap-3 sm:grid-cols-2">
          {actions.map((action) => {
            const Icon = action.icon
            return (
              <li key={action.title}>
                <Link
                  href={action.href}
                  className="group flex h-full items-start gap-3 rounded-lg border border-line bg-surface p-4 transition-colors hover:border-accent"
                >
                  <span className="flex size-9 shrink-0 items-center justify-center rounded-md bg-accent-soft text-accent-soft-ink">
                    <Icon className="size-4.5" aria-hidden="true" />
                  </span>
                  <span className="min-w-0 flex-1">
                    <span className="flex flex-wrap items-center gap-2">
                      <span className="text-sm font-semibold text-ink-strong group-hover:text-accent">
                        {action.title}
                      </span>
                      {action.badge ? <Badge size="sm">{action.badge}</Badge> : null}
                    </span>
                    <span className="mt-0.5 block text-xs text-ink-muted">
                      {action.description}
                    </span>
                  </span>
                  <ArrowRight
                    className="size-4 shrink-0 text-ink-subtle transition-transform group-hover:translate-x-0.5"
                    aria-hidden="true"
                  />
                </Link>
              </li>
            )
          })}
        </ul>
      </Panel>
    </section>
  )
}

components/ui/card.tsx

import type { HTMLAttributes, ReactNode, ElementType } from 'react'
import { cn } from '@/lib/cn'

/**
 * Card & Panel
 *
 * Two containment primitives with deliberately different jobs:
 *
 *   Card  — a discrete, often interactive record in a collection.
 *   Panel — a titled region of a page, with an optional header action row.
 *
 * Keeping them separate is what stops the library from degenerating into
 * "everything is a rounded box with a shadow".
 */

export interface CardProps extends HTMLAttributes<HTMLDivElement> {
  as?: ElementType
  /** Forwarded when `as` renders a link. */
  href?: string
  /** `outline` is the default; `raised` adds elevation; `sunken` insets. */
  tone?: 'outline' | 'raised' | 'sunken' | 'ghost' | 'accent'
  /** Adds hover affordance. Only use when the whole card is a link/button. */
  interactive?: boolean
  padding?: 'none' | 'sm' | 'md' | 'lg'
}

const cardTones = {
  outline: 'bg-surface border border-line',
  raised: 'bg-surface-raised border border-line shadow-sm',
  sunken: 'bg-surface-sunken border border-line-subtle',
  ghost: 'bg-transparent border border-transparent',
  accent: 'bg-accent-soft border border-accent-line',
} as const

const cardPadding = {
  none: 'p-0',
  sm: 'p-3',
  md: 'p-card',
  lg: 'p-6 sm:p-8',
} as const

export function Card({
  as: Tag = 'div',
  tone = 'outline',
  interactive = false,
  padding = 'md',
  className,
  children,
  ...props
}: CardProps) {
  return (
    <Tag
      className={cn(
        'rounded-lg',
        cardTones[tone],
        cardPadding[padding],
        interactive &&
          'transition-[border-color,box-shadow,background-color] duration-150 ease-standard hover:border-line-strong hover:shadow-sm',
        className,
      )}
      {...props}
    >
      {children}
    </Tag>
  )
}

export function CardHeader({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
  return (
    <div className={cn('flex items-start justify-between gap-4', className)} {...props}>
      {children}
    </div>
  )
}

export function CardTitle({
  as: Tag = 'h3',
  className,
  children,
  ...props
}: HTMLAttributes<HTMLHeadingElement> & { as?: ElementType }) {
  return (
    <Tag className={cn('text-md leading-snug font-semibold text-ink-strong', className)} {...props}>
      {children}
    </Tag>
  )
}

export function CardDescription({
  className,
  children,
  ...props
}: HTMLAttributes<HTMLParagraphElement>) {
  return (
    <p className={cn('text-sm leading-normal text-ink-muted', className)} {...props}>
      {children}
    </p>
  )
}

export function CardFooter({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn(
        'mt-4 flex flex-wrap items-center gap-3 border-t border-line-subtle pt-4',
        className,
      )}
      {...props}
    >
      {children}
    </div>
  )
}

export interface PanelProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {
  title: ReactNode
  description?: ReactNode
  /** Rendered on the right of the panel header. */
  action?: ReactNode
  /** Removes body padding — for tables and lists that manage their own. */
  flush?: boolean
  as?: ElementType
  headingLevel?: 'h2' | 'h3' | 'h4'
}

export function Panel({
  title,
  description,
  action,
  flush = false,
  as: Tag = 'section',
  headingLevel: Heading = 'h3',
  className,
  children,
  ...props
}: PanelProps) {
  return (
    <Tag
      className={cn('overflow-hidden rounded-lg border border-line bg-surface', className)}
      {...props}
    >
      <div className="flex flex-wrap items-start justify-between gap-3 border-b border-line-subtle bg-surface-sunken px-4 py-3">
        <div className="min-w-0">
          <Heading className="text-sm font-semibold text-ink-strong">{title}</Heading>
          {description ? <p className="mt-0.5 text-xs text-ink-muted">{description}</p> : null}
        </div>
        {action ? <div className="flex shrink-0 items-center gap-2">{action}</div> : null}
      </div>
      <div className={cn(flush ? '' : 'p-card')}>{children}</div>
    </Tag>
  )
}

Demo source — adapt to your project. Foundry is not published as a package.

Usage

Placing the four most common tasks on the overview removes two navigation steps from them — which is the only justification a quick-action panel ever has.

  • Four actions. A quick-action panel with nine is a navigation menu.
  • Mark the most used one; it is the reason the panel exists.

Variants and states

Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.

  • Four actions
  • Most-used badge
  • Two columns

Accessibility

Whole-card links
Each action is one focus stop with a descriptive name.
Decorative icons
Icons reinforce the label and are hidden.

Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.