Skip to content

Inline partial failure

One widget fails while the rest of the page stays intact.

Statesintermediateerrorpartialretryresilience

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.

'use client'

import { useState } from 'react'
import { RefreshCw } from 'lucide-react'
import { Alert } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Panel } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Metric } from '@/components/ui/metric'

/**
 * Inline partial failure
 *
 * One widget fails while the rest of the page is fine. Replacing the whole
 * screen with an error page for a single failed panel loses everything that did
 * load — so the failure is contained, explained and retryable in place.
 */
export default function InlineFailureSection() {
  const [state, setState] = useState<'failed' | 'retrying' | 'recovered'>('failed')

  const retry = () => {
    setState('retrying')
    setTimeout(() => setState('recovered'), 1200)
  }

  return (
    <section className="bg-canvas p-4 sm:p-6">
      <div className="grid gap-3 sm:grid-cols-2">
        <Metric
          label="Active seats"
          value="1,284"
          delta={4.1}
          sparkline={[40, 42, 41, 45, 47, 49, 52, 54]}
        />

        <Panel title="Revenue" description="Last 30 days" headingLevel="h2">
          {state === 'failed' ? (
            <Alert
              tone="danger"
              title="Could not load revenue"
              actions={
                <Button
                  size="sm"
                  variant="outline"
                  onClick={retry}
                  leadingIcon={<RefreshCw className="size-3.5" />}
                >
                  Retry
                </Button>
              }
            >
              The billing service did not respond. Everything else on this page loaded normally.
            </Alert>
          ) : null}

          {state === 'retrying' ? (
            <div className="flex flex-col gap-3">
              <p className="sr-only" role="status">
                Retrying
              </p>
              <Skeleton width="45%" height="1.75rem" />
              <Skeleton width="30%" />
            </div>
          ) : null}

          {state === 'recovered' ? (
            <Metric
              appearance="plain"
              label="Monthly recurring revenue"
              value="$184,200"
              delta={12.4}
              deltaLabel="vs last month"
            />
          ) : null}
        </Panel>
      </div>

      <p className="mt-4 text-xs text-ink-subtle">
        Demo only — the failure and recovery are simulated locally.
      </p>
    </section>
  )
}

components/blocks/sections/error/inline-failure.tsx

'use client'

import { useState } from 'react'
import { RefreshCw } from 'lucide-react'
import { Alert } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Panel } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { Metric } from '@/components/ui/metric'

/**
 * Inline partial failure
 *
 * One widget fails while the rest of the page is fine. Replacing the whole
 * screen with an error page for a single failed panel loses everything that did
 * load — so the failure is contained, explained and retryable in place.
 */
export default function InlineFailureSection() {
  const [state, setState] = useState<'failed' | 'retrying' | 'recovered'>('failed')

  const retry = () => {
    setState('retrying')
    setTimeout(() => setState('recovered'), 1200)
  }

  return (
    <section className="bg-canvas p-4 sm:p-6">
      <div className="grid gap-3 sm:grid-cols-2">
        <Metric
          label="Active seats"
          value="1,284"
          delta={4.1}
          sparkline={[40, 42, 41, 45, 47, 49, 52, 54]}
        />

        <Panel title="Revenue" description="Last 30 days" headingLevel="h2">
          {state === 'failed' ? (
            <Alert
              tone="danger"
              title="Could not load revenue"
              actions={
                <Button
                  size="sm"
                  variant="outline"
                  onClick={retry}
                  leadingIcon={<RefreshCw className="size-3.5" />}
                >
                  Retry
                </Button>
              }
            >
              The billing service did not respond. Everything else on this page loaded normally.
            </Alert>
          ) : null}

          {state === 'retrying' ? (
            <div className="flex flex-col gap-3">
              <p className="sr-only" role="status">
                Retrying
              </p>
              <Skeleton width="45%" height="1.75rem" />
              <Skeleton width="30%" />
            </div>
          ) : null}

          {state === 'recovered' ? (
            <Metric
              appearance="plain"
              label="Monthly recurring revenue"
              value="$184,200"
              delta={12.4}
              deltaLabel="vs last month"
            />
          ) : null}
        </Panel>
      </div>

      <p className="mt-4 text-xs text-ink-subtle">
        Demo only — the failure and recovery are simulated locally.
      </p>
    </section>
  )
}

components/ui/alert.tsx

import type { ReactNode } from 'react'
import { Info, CheckCircle2, AlertTriangle, OctagonAlert } from 'lucide-react'
import { cn } from '@/lib/cn'

/**
 * Alert
 *
 * A block-level message attached to a region of the page. The icon is chosen
 * from the tone, and `role` switches to `alert` for danger so screen readers
 * interrupt — informational tones use the polite `status` role instead.
 */
export type AlertTone = 'info' | 'success' | 'warning' | 'danger' | 'neutral'

export interface AlertProps {
  tone?: AlertTone
  title?: ReactNode
  children?: ReactNode
  /** Rendered under the body — usually one or two buttons. */
  actions?: ReactNode
  /** Slot for a dismiss control supplied by the caller. */
  trailing?: ReactNode
  className?: string
  /** Force the live-region politeness rather than deriving it from tone. */
  live?: 'off' | 'polite' | 'assertive'
}

const tones = {
  info: { icon: Info, surface: 'bg-info-soft border-info-line', accent: 'text-info' },
  success: {
    icon: CheckCircle2,
    surface: 'bg-success-soft border-success-line',
    accent: 'text-success',
  },
  warning: {
    icon: AlertTriangle,
    surface: 'bg-warning-soft border-warning-line',
    accent: 'text-warning',
  },
  danger: {
    icon: OctagonAlert,
    surface: 'bg-danger-soft border-danger-line',
    accent: 'text-danger',
  },
  neutral: { icon: Info, surface: 'bg-surface-sunken border-line', accent: 'text-ink-muted' },
} as const

export function Alert({
  tone = 'info',
  title,
  children,
  actions,
  trailing,
  className,
  live,
}: AlertProps) {
  const entry = tones[tone]
  const Icon = entry.icon
  const politeness = live ?? (tone === 'danger' ? 'assertive' : 'polite')

  return (
    <div
      role={tone === 'danger' ? 'alert' : 'status'}
      aria-live={politeness === 'off' ? undefined : politeness}
      className={cn('flex gap-3 rounded-md border p-3 text-sm', entry.surface, className)}
    >
      <Icon className={cn('mt-0.5 size-4 shrink-0', entry.accent)} aria-hidden="true" />
      <div className="min-w-0 flex-1">
        {title ? <p className="font-semibold text-ink-strong">{title}</p> : null}
        {children ? <div className={cn('text-ink-muted', title && 'mt-1')}>{children}</div> : null}
        {actions ? <div className="mt-3 flex flex-wrap gap-2">{actions}</div> : null}
      </div>
      {trailing ? <div className="shrink-0">{trailing}</div> : null}
    </div>
  )
}

components/ui/skeleton.tsx

import { cn } from '@/lib/cn'

/**
 * Skeleton
 *
 * Placeholder geometry for content that is still resolving. The wrapper is
 * marked `aria-hidden` and callers are expected to expose a single polite
 * "Loading" message instead — a screen reader should never have to listen to
 * eleven grey rectangles.
 */
export interface SkeletonProps {
  variant?: 'line' | 'block' | 'circle' | 'button'
  width?: string
  height?: string
  className?: string
}

export function Skeleton({ variant = 'line', width, height, className }: SkeletonProps) {
  const shape = {
    line: 'h-3 rounded-sm',
    block: 'h-24 rounded-md',
    circle: 'size-10 rounded-full',
    button: 'h-control w-24 rounded-md',
  }[variant]

  return (
    <div
      aria-hidden="true"
      className={cn('animate-pulse-token bg-surface-sunken', shape, className)}
      style={{ width, height }}
    />
  )
}

export interface SkeletonTextProps {
  lines?: number
  className?: string
}

/** Paragraph placeholder with a deliberately short final line. */
export function SkeletonText({ lines = 3, className }: SkeletonTextProps) {
  return (
    <div className={cn('flex flex-col gap-2', className)} aria-hidden="true">
      {Array.from({ length: lines }).map((_, index) => (
        <Skeleton key={index} width={index === lines - 1 ? '60%' : '100%'} />
      ))}
    </div>
  )
}

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

Usage

Replacing a whole screen for a single failed panel loses everything that did load, so the failure is contained, explained and retryable in place.

  • Say what still worked; 'something went wrong' implies everything did.
  • Show a skeleton while retrying, so the retry is visibly happening.

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.

  • Contained failure
  • Retrying skeleton
  • Recovered state

Accessibility

Contained alert
The failure Alert is scoped to the panel, not the page.
Retry status
The retrying state announces itself politely.

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.