Skip to content
Product block

Alert Rule List

Threshold alerts with the two facts most rule lists omit: how often each has fired, and whether its channel is muted — so a rule that is on but delivering nowhere reads as its own state.

282 lineslucide-reactAdded 31 Aug 2026Updated 11 Sept 2026
  • alerts
  • monitoring
  • thresholds
  • dashboard
  • rules

What's included

  • components/dashboard-alert-rules.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add dashboard-alert-rules

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Alert rules

4 of 5 active · 1 delivering nowhere

  • critical

    API error rate

    Alert when API error rate stays above 2% for 5 minutes.

    #incidentsFired 3 times in the last 30 days
  • warning

    p95 latency

    Alert when p95 latency stays above 800ms for 15 minutes.

    #platform-alertsFired 27 times in the last 30 days

    This rule is on, but #platform-alerts is muted — nothing has been delivered since it was muted.

  • warning

    Signups per hour

    Alert when Signups per hour stays below 4 for 1 hour.

    growth@acme.comFired 142 times in the last 30 days

    Firing 142 times a month is usually a threshold problem rather than an incident. Consider raising it before muting the channel.

  • critical

    Job queue depth

    Alert when Job queue depth stays above 5,000 for 10 minutes.

    pagerduty.com/…/v2Fired 1 times in the last 30 days
  • info

    Storage used

    Alert when Storage used stays above 85% for 1 day.

    ops@acme.comFired 0 times in the last 30 days

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/dashboard-alert-rules.tsx
'use client'

/**
 * <DashboardAlertRules> — thresholds that page someone, listed honestly.
 *
 * The dashboard tier can show you a number going wrong. It cannot tell you
 * that a number went wrong at 3am on Sunday, and every product that reaches
 * a second customer grows alert rules for exactly that reason.
 *
 * THE THING ALERT UIs USUALLY GET WRONG
 *
 * They render a rule as a sentence and a toggle, and omit the two facts
 * that decide whether the rule is any good: how often it has fired, and
 * where it goes. A rule that fired 140 times last month is not a rule, it
 * is noise with a cron attached — and the person who muted the channel it
 * posts to is the only one who knows. Both are on the row here, and a rule
 * over the noise threshold is visibly marked rather than left for someone
 * to notice.
 *
 * MUTED IS NOT OFF
 *
 * The third state is the important one. A rule can be enabled and still
 * deliver nowhere, because its channel was muted somewhere else entirely.
 * Most UIs show that rule as a healthy green toggle. Here it renders as its
 * own state with its own sentence, because "on but going nowhere" is the
 * state that gets an incident missed.
 *
 * NO DESTRUCTIVE DEFAULT. Deleting a rule is behind the row menu rather
 * than a trash icon sitting under the cursor's natural resting place, and
 * disabling is offered first — a rule that is wrong at 3am is usually
 * wrong in its threshold, not in its existence.
 *
 * ACCESSIBILITY: the toggles are real checkboxes with `role="switch"` so
 * their state is announced rather than implied by colour; the firing counts
 * carry a visually hidden qualifier so "140" is read as "140 times in the
 * last 30 days"; and severity is a word as well as a colour.
 */

import * as React from 'react'
import { AlertTriangle, BellOff, Hash, Mail, MoreHorizontal, Webhook } from 'lucide-react'

export type AlertSeverity = 'critical' | 'warning' | 'info'
export type AlertChannel = 'email' | 'slack' | 'webhook'

export interface AlertRule {
  id: string
  metric: string
  comparator: 'above' | 'below'
  threshold: string
  /** Window the threshold is evaluated over, e.g. "5 minutes". */
  window: string
  severity: AlertSeverity
  channel: AlertChannel
  channelTarget: string
  enabled: boolean
  /** True when the destination is muted elsewhere — see the header. */
  channelMuted?: boolean
  /** Times this rule fired in the last 30 days. */
  firedCount: number
}

export interface DashboardAlertRulesProps {
  rules?: AlertRule[]
  /** Firings above which a rule is called out as noisy. */
  noisyThreshold?: number
  className?: string
}

const DEFAULT_RULES: AlertRule[] = [
  {
    id: 'error-rate',
    metric: 'API error rate',
    comparator: 'above',
    threshold: '2%',
    window: '5 minutes',
    severity: 'critical',
    channel: 'slack',
    channelTarget: '#incidents',
    enabled: true,
    firedCount: 3,
  },
  {
    id: 'p95',
    metric: 'p95 latency',
    comparator: 'above',
    threshold: '800ms',
    window: '15 minutes',
    severity: 'warning',
    channel: 'slack',
    channelTarget: '#platform-alerts',
    enabled: true,
    channelMuted: true,
    firedCount: 27,
  },
  {
    id: 'signups',
    metric: 'Signups per hour',
    comparator: 'below',
    threshold: '4',
    window: '1 hour',
    severity: 'warning',
    channel: 'email',
    channelTarget: 'growth@acme.com',
    enabled: true,
    firedCount: 142,
  },
  {
    id: 'queue',
    metric: 'Job queue depth',
    comparator: 'above',
    threshold: '5,000',
    window: '10 minutes',
    severity: 'critical',
    channel: 'webhook',
    channelTarget: 'pagerduty.com/…/v2',
    enabled: true,
    firedCount: 1,
  },
  {
    id: 'storage',
    metric: 'Storage used',
    comparator: 'above',
    threshold: '85%',
    window: '1 day',
    severity: 'info',
    channel: 'email',
    channelTarget: 'ops@acme.com',
    enabled: false,
    firedCount: 0,
  },
]

const SEVERITY_STYLES: Record<AlertSeverity, string> = {
  critical: 'bg-destructive/10 text-destructive',
  warning: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
  info: 'bg-muted text-muted-foreground',
}

const CHANNEL_ICON: Record<AlertChannel, typeof Mail> = {
  email: Mail,
  slack: Hash,
  webhook: Webhook,
}

export function DashboardAlertRules({
  rules = DEFAULT_RULES,
  noisyThreshold = 30,
  className = '',
}: DashboardAlertRulesProps) {
  const [state, setState] = React.useState(rules)

  const toggle = (id: string) =>
    setState((current) =>
      current.map((rule) => (rule.id === id ? { ...rule, enabled: !rule.enabled } : rule)),
    )

  const active = state.filter((rule) => rule.enabled).length
  const undelivered = state.filter((rule) => rule.enabled && rule.channelMuted).length

  return (
    <section
      className={`rounded-2xl border border-border bg-card text-card-foreground ${className}`}
    >
      <header className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-5 sm:p-6">
        <div>
          <h2 className="text-base font-semibold">Alert rules</h2>
          <p className="mt-1 text-sm text-muted-foreground">
            {active} of {state.length} active
            {undelivered > 0 ? (
              <>
                {' · '}
                <span className="font-medium text-amber-600 dark:text-amber-400">
                  {undelivered} delivering nowhere
                </span>
              </>
            ) : null}
          </p>
        </div>
        <button
          type="button"
          className="rounded-lg bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
        >
          New rule
        </button>
      </header>

      <ul className="divide-y divide-border">
        {state.map((rule) => {
          const ChannelIcon = CHANNEL_ICON[rule.channel]
          const noisy = rule.firedCount >= noisyThreshold
          const dark = rule.enabled && rule.channelMuted

          return (
            <li key={rule.id} className="flex flex-wrap items-start gap-4 p-5 sm:p-6">
              <div className="min-w-0 flex-1">
                <div className="flex flex-wrap items-center gap-2">
                  <span
                    className={`rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide ${SEVERITY_STYLES[rule.severity]}`}
                  >
                    {rule.severity}
                  </span>
                  <h3 data-stress-ignore className="text-sm font-semibold">{rule.metric}</h3>
                </div>

                {/* The rule as a sentence — the form people actually verify. */}
                <p className="mt-1.5 text-sm text-muted-foreground">
                  Alert when {rule.metric} stays{' '}
                  <span className="font-medium text-foreground">
                    {rule.comparator} {rule.threshold}
                  </span>{' '}
                  for {rule.window}.
                </p>

                <div className="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
                  <span className="inline-flex items-center gap-1.5">
                    <ChannelIcon aria-hidden className="h-3.5 w-3.5" />
                    {rule.channelTarget}
                  </span>
                  <span className={noisy ? 'font-medium text-amber-600 dark:text-amber-400' : ''}>
                    Fired {rule.firedCount}
                    <span className="sr-only"> times in the last 30 days</span>
                    <span aria-hidden> × / 30d</span>
                  </span>
                </div>

                {/*
                  The two states worth a sentence of their own. Both are
                  conditions a green toggle would otherwise hide.
                */}
                {dark ? (
                  <p className="mt-3 flex items-start gap-2 rounded-lg bg-amber-500/10 p-2.5 text-xs text-amber-700 dark:text-amber-300">
                    <BellOff aria-hidden className="mt-0.5 h-3.5 w-3.5 shrink-0" />
                    This rule is on, but {rule.channelTarget} is muted — nothing has been
                    delivered since it was muted.
                  </p>
                ) : null}

                {noisy && !dark ? (
                  <p className="mt-3 flex items-start gap-2 rounded-lg bg-muted p-2.5 text-xs text-muted-foreground">
                    <AlertTriangle aria-hidden className="mt-0.5 h-3.5 w-3.5 shrink-0" />
                    Firing {rule.firedCount} times a month is usually a threshold problem
                    rather than an incident. Consider raising it before muting the channel.
                  </p>
                ) : null}
              </div>

              <div className="flex items-center gap-1.5">
                <button
                  type="button"
                  role="switch"
                  aria-checked={rule.enabled}
                  aria-label={`${rule.enabled ? 'Disable' : 'Enable'} ${rule.metric} alert`}
                  onClick={() => toggle(rule.id)}
                  className={`relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background ${
                    rule.enabled ? 'bg-primary' : 'bg-muted-foreground/30'
                  }`}
                >
                  <span
                    aria-hidden
                    className={`absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform ${
                      rule.enabled
                        ? 'translate-x-[22px] rtl:-translate-x-[22px]'
                        : 'translate-x-[2px] rtl:-translate-x-[2px]'
                    }`}
                  />
                </button>
                <button
                  type="button"
                  aria-label={`More actions for ${rule.metric}`}
                  className="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                >
                  <MoreHorizontal aria-hidden className="h-4 w-4" />
                </button>
              </div>
            </li>
          )
        })}
      </ul>
    </section>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/dashboard-alert-rules.tsx and import it where you need the section:

import { DashboardAlertRules } from '@/components/dashboard-alert-rules'

Customize

1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
rulesAlertRule[]DEFAULT_RULES
noisyThresholdFirings above which a rule is called out as noisy.number30
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

dashboard-alert-rules.html
<!--
  Alert Rule List — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<section class="rounded-2xl border border-border bg-card text-card-foreground ">
  <header class="flex flex-wrap items-center justify-between gap-3 border-b border-border p-5 sm:p-6">
    <div>
      <h2 class="text-base font-semibold">Alert rules</h2>
      <p class="mt-1 text-sm text-muted-foreground">4 of 5 active · <span class="font-medium text-amber-600 dark:text-amber-400">1 delivering nowhere</span></p>
    </div>
    <button type="button" class="rounded-lg bg-primary px-3 py-1.5 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">New rule</button>
  </header>
  <ul class="divide-y divide-border">
    <li class="flex flex-wrap items-start gap-4 p-5 sm:p-6">
      <div class="min-w-0 flex-1">
        <div class="flex flex-wrap items-center gap-2">
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-destructive/10 text-destructive">critical</span>
          <h3 data-stress-ignore="true" class="text-sm font-semibold">API error rate</h3>
        </div>
        <p class="mt-1.5 text-sm text-muted-foreground">Alert when API error rate stays <span class="font-medium text-foreground">above 2%</span> for 5 minutes.</p>
        <div class="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
          <span class="inline-flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hash h-3.5 w-3.5" aria-hidden="true"><line x1="4" x2="20" y1="9" y2="9"></line><line x1="4" x2="20" y1="15" y2="15"></line><line x1="10" x2="8" y1="3" y2="21"></line><line x1="16" x2="14" y1="3" y2="21"></line></svg>#incidents</span>
          <span class="">Fired 3<span class="sr-only"> times in the last 30 days</span><span aria-hidden="true"> × / 30d</span></span>
        </div>
      </div>
      <div class="flex items-center gap-1.5">
        <button type="button" role="switch" aria-checked="true" aria-label="Disable API error rate alert" class="relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-primary">
          <span aria-hidden="true" class="absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform translate-x-[22px] rtl:-translate-x-[22px]"></span>
        </button>
        <button type="button" aria-label="More actions for API error rate" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="1"></circle>
            <circle cx="19" cy="12" r="1"></circle>
            <circle cx="5" cy="12" r="1"></circle>
          </svg>
        </button>
      </div>
    </li>
    <li class="flex flex-wrap items-start gap-4 p-5 sm:p-6">
      <div class="min-w-0 flex-1">
        <div class="flex flex-wrap items-center gap-2">
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-amber-500/10 text-amber-600 dark:text-amber-400">warning</span>
          <h3 data-stress-ignore="true" class="text-sm font-semibold">p95 latency</h3>
        </div>
        <p class="mt-1.5 text-sm text-muted-foreground">Alert when p95 latency stays <span class="font-medium text-foreground">above 800ms</span> for 15 minutes.</p>
        <div class="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
          <span class="inline-flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hash h-3.5 w-3.5" aria-hidden="true"><line x1="4" x2="20" y1="9" y2="9"></line><line x1="4" x2="20" y1="15" y2="15"></line><line x1="10" x2="8" y1="3" y2="21"></line><line x1="16" x2="14" y1="3" y2="21"></line></svg>#platform-alerts</span>
          <span class="">Fired 27<span class="sr-only"> times in the last 30 days</span><span aria-hidden="true"> × / 30d</span></span>
        </div>
        <p class="mt-3 flex items-start gap-2 rounded-lg bg-amber-500/10 p-2.5 text-xs text-amber-700 dark:text-amber-300"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bell-off mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"></path><path d="M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"></path><path d="m2 2 20 20"></path><path d="M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"></path></svg>This rule is on, but #platform-alerts is muted — nothing has been delivered since it was muted.</p>
      </div>
      <div class="flex items-center gap-1.5">
        <button type="button" role="switch" aria-checked="true" aria-label="Disable p95 latency alert" class="relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-primary">
          <span aria-hidden="true" class="absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform translate-x-[22px] rtl:-translate-x-[22px]"></span>
        </button>
        <button type="button" aria-label="More actions for p95 latency" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="1"></circle>
            <circle cx="19" cy="12" r="1"></circle>
            <circle cx="5" cy="12" r="1"></circle>
          </svg>
        </button>
      </div>
    </li>
    <li class="flex flex-wrap items-start gap-4 p-5 sm:p-6">
      <div class="min-w-0 flex-1">
        <div class="flex flex-wrap items-center gap-2">
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-amber-500/10 text-amber-600 dark:text-amber-400">warning</span>
          <h3 data-stress-ignore="true" class="text-sm font-semibold">Signups per hour</h3>
        </div>
        <p class="mt-1.5 text-sm text-muted-foreground">Alert when Signups per hour stays <span class="font-medium text-foreground">below 4</span> for 1 hour.</p>
        <div class="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
          <span class="inline-flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail h-3.5 w-3.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>growth@acme.com</span>
          <span class="font-medium text-amber-600 dark:text-amber-400">Fired 142<span class="sr-only"> times in the last 30 days</span><span aria-hidden="true"> × / 30d</span></span>
        </div>
        <p class="mt-3 flex items-start gap-2 rounded-lg bg-muted p-2.5 text-xs text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-triangle-alert mt-0.5 h-3.5 w-3.5 shrink-0" aria-hidden="true"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>Firing 142 times a month is usually a threshold problem rather than an incident. Consider raising it before muting the channel.</p>
      </div>
      <div class="flex items-center gap-1.5">
        <button type="button" role="switch" aria-checked="true" aria-label="Disable Signups per hour alert" class="relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-primary">
          <span aria-hidden="true" class="absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform translate-x-[22px] rtl:-translate-x-[22px]"></span>
        </button>
        <button type="button" aria-label="More actions for Signups per hour" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="1"></circle>
            <circle cx="19" cy="12" r="1"></circle>
            <circle cx="5" cy="12" r="1"></circle>
          </svg>
        </button>
      </div>
    </li>
    <li class="flex flex-wrap items-start gap-4 p-5 sm:p-6">
      <div class="min-w-0 flex-1">
        <div class="flex flex-wrap items-center gap-2">
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-destructive/10 text-destructive">critical</span>
          <h3 data-stress-ignore="true" class="text-sm font-semibold">Job queue depth</h3>
        </div>
        <p class="mt-1.5 text-sm text-muted-foreground">Alert when Job queue depth stays <span class="font-medium text-foreground">above 5,000</span> for 10 minutes.</p>
        <div class="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
          <span class="inline-flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-webhook h-3.5 w-3.5" aria-hidden="true"><path d="M18 16.98h-5.99c-1.1 0-1.95.94-2.48 1.9A4 4 0 0 1 2 17c.01-.7.2-1.4.57-2"></path><path d="m6 17 3.13-5.78c.53-.97.1-2.18-.5-3.1a4 4 0 1 1 6.89-4.06"></path><path d="m12 6 3.13 5.73C15.66 12.7 16.9 13 18 13a4 4 0 0 1 0 8"></path></svg>pagerduty.com/…/v2</span>
          <span class="">Fired 1<span class="sr-only"> times in the last 30 days</span><span aria-hidden="true"> × / 30d</span></span>
        </div>
      </div>
      <div class="flex items-center gap-1.5">
        <button type="button" role="switch" aria-checked="true" aria-label="Disable Job queue depth alert" class="relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-primary">
          <span aria-hidden="true" class="absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform translate-x-[22px] rtl:-translate-x-[22px]"></span>
        </button>
        <button type="button" aria-label="More actions for Job queue depth" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="1"></circle>
            <circle cx="19" cy="12" r="1"></circle>
            <circle cx="5" cy="12" r="1"></circle>
          </svg>
        </button>
      </div>
    </li>
    <li class="flex flex-wrap items-start gap-4 p-5 sm:p-6">
      <div class="min-w-0 flex-1">
        <div class="flex flex-wrap items-center gap-2">
          <span class="rounded px-1.5 py-0.5 text-[11px] font-semibold uppercase tracking-wide bg-muted text-muted-foreground">info</span>
          <h3 data-stress-ignore="true" class="text-sm font-semibold">Storage used</h3>
        </div>
        <p class="mt-1.5 text-sm text-muted-foreground">Alert when Storage used stays <span class="font-medium text-foreground">above 85%</span> for 1 day.</p>
        <div class="mt-2.5 flex flex-wrap items-center gap-x-4 gap-y-1.5 text-xs text-muted-foreground">
          <span class="inline-flex items-center gap-1.5"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail h-3.5 w-3.5" aria-hidden="true"><path d="m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7"></path><rect x="2" y="4" width="20" height="16" rx="2"></rect></svg>ops@acme.com</span>
          <span class="">Fired 0<span class="sr-only"> times in the last 30 days</span><span aria-hidden="true"> × / 30d</span></span>
        </div>
      </div>
      <div class="flex items-center gap-1.5">
        <button type="button" role="switch" aria-checked="false" aria-label="Enable Storage used alert" class="relative h-6 w-11 rounded-full border border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background bg-muted-foreground/30">
          <span aria-hidden="true" class="absolute start-0 top-0.5 h-5 w-5 rounded-full border border-transparent bg-background transition-transform translate-x-[2px] rtl:-translate-x-[2px]"></span>
        </button>
        <button type="button" aria-label="More actions for Storage used" class="rounded-lg p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">
          <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ellipsis h-4 w-4" aria-hidden="true">
            <circle cx="12" cy="12" r="1"></circle>
            <circle cx="19" cy="12" r="1"></circle>
            <circle cx="5" cy="12" r="1"></circle>
          </svg>
        </button>
      </div>
    </li>
  </ul>
</section>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

8 more blocks in Dashboards

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Dashboards

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Dashboards blocks

View category
Open the full page for this block

KPI Cards With Deltas

Metric row where each card knows whether a rise is good news, so churn going up is red while revenue going up is green.

Dashboards114 lines1 dep
Open the full page for this block

Activity Feed Panel

Recent-events list with typed icons and relative timestamps that keep the absolute value in a <time> element.

Dashboards171 lines1 dep
Open the full page for this block

Kanban Board Columns

Four columns of prioritised, tagged, assigned task cards with a blocked state — the board as a layout contract, leaving drag behaviour to your own state and library.

Dashboards203 lines1 dep
Open the full page for this block

Audit Log Timeline

Events grouped by day on a vertical rail, icons varying by type and one entry expanded into a detail card — what keeps forty events scannable instead of forty gray dots.

Dashboards204 lines1 dep
Open the full page for this block

Saved View Tabs

Saved views with the third state most implementations drop: a shared view with unsaved edits, where Save changes what a colleague sees tomorrow. Two honest buttons instead of one, ownership on the tab, and dirty said in words.

Dashboards306 lines1 dep
Open the full page for this block

Comparison Period Picker

A reporting window with the comparison window spelled out beside it — resolved dates, UTC arithmetic, and an option to align weekdays so a weekend is never compared against a Tuesday.

Dashboards315 lines1 dep