Dashboard quick actions
The four most common tasks, as full-card links.
Live preview
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.
Related
All sectionsSetup checklist
In-product onboarding whose progress is computed from the items.
starter3 variantsDashboard metric row
Four KPI tiles, two of them with inverted trend semantics.
starterFeatured3 variantsAdmin starter
An internal console: overview, a filterable member table, orders, analytics and settings — all at compact density on the same components as the marketing starters.
advancedFeatured4 variants