Skip to content

Latest commit

 

History

History
285 lines (214 loc) · 12 KB

File metadata and controls

285 lines (214 loc) · 12 KB

Error UI Pattern Guide

This document defines the standardized error surfaces in the Credence Frontend application. It is written for frontend contributors building or modifying UI components, forms, and pages.

For general state priority (loading/empty/error), refer to the UI States Guide. For copy and microcopy phrasing guidelines, see the Copy Tone Guide.


Decision Matrix: Which Surface to Use?

Choose the error surface based on scope, user impact, and persistence:

Surface Scope Typical Trigger Persistence Component / API
Inline Form Error Single input field Real-time or submit input validation failure Persistent until input is corrected FormField
Banner Alert Section or page Contextual condition (network mismatch, read-only mode, API degradation) Persistent or dismissible within section Banner
Toast Notification Global overlay Result of user action (transaction failure, network error on click) Auto-dismissing or manual dismiss useToast() / Toast
Section / Page Error Whole panel or page Failed data fetch, 404, or unhandled UI crash Persistent until retry succeeds ErrorState / ErrorBoundary

1. Inline Form Errors (FormField)

Use Case

Use inline form errors when user input fails validation (e.g., an invalid Stellar public key, out-of-range USDC amount, or missing required field).

Accessibility & Behavior

  • Error message is linked to the input via aria-describedby.
  • Sets aria-invalid="true" on the form control.
  • Renders with role="alert" so screen readers announce the validation message immediately.
  • Styled using danger CSS design tokens (var(--credence-color-danger-text)).
  • For successful validation confirmations, use FormField's success prop instead: it links ${id}-success via aria-describedby, uses role="status", and does not set aria-invalid. If both error and success are provided, error takes precedence.

Code Example

import { useState } from 'react'
import { FormField, Input } from '../components/forms'
import { isValidStellarAddress } from '../lib/stellar'

export function RecipientAddressField() {
  const [address, setAddress] = useState('')
  const [touched, setTouched] = useState(false)

  const isValid = isValidStellarAddress(address)
  const errorMessage =
    touched && !isValid && address.length > 0
      ? 'Enter a valid 56-character Stellar public key starting with G.'
      : undefined

  return (
    <FormField
      id="recipient-address"
      label="Recipient Address"
      hint="Must be a valid G... public key"
      error={errorMessage}
      success={touched && isValid ? 'Valid Stellar address' : undefined}
    >
      <Input
        value={address}
        onChange={(e) => setAddress(e.target.value)}
        onBlur={() => setTouched(true)}
        placeholder="G..."
        spellCheck={false}
        autoComplete="off"
      />
    </FormField>
  )
}

2. Banner Alerts (Banner)

Use Case

Use banners for page-level or container-level contextual messages that require user attention but do not prevent the rest of the layout from rendering (e.g., wallet network mismatch, unconfirmed pending transactions, or read-only status).

Accessibility & Behavior

  • Critical (severity="critical") and warning (severity="warning") banners set role="alert".
  • Information and success banners set role="status".
  • Banners support inline actions (action={{ label: 'Switch Network', onClick: handleSwitch }}).
  • Dismissible banners support focus restoration via returnFocusRef and keyboard escape (Escape key).

Code Example

import { useRef } from 'react'
import Banner from '../components/Banner'
import { useWallet } from '../hooks/useWallet'

export function NetworkCheckBanner() {
  const { network, switchNetwork } = useWallet()
  const triggerRef = useRef<HTMLButtonElement>(null)

  if (network === 'mainnet') return null

  return (
    <Banner
      severity="warning"
      title="Testnet Active"
      dismissible
      returnFocusRef={triggerRef}
      action={{
        label: 'Switch to Mainnet',
        onClick: switchNetwork,
      }}
    >
      You are connected to Soroban Testnet. Actions will not affect live USDC balances.
    </Banner>
  )
}

3. Toast Notifications (Toast / useToast)

Use Case

Use toasts for short, non-blocking feedback following an asynchronous user action (e.g., wallet submission failed, link copied to clipboard, or settings saved).

Accessibility & Behavior

  • Injected into the top-right overlay stack via ToastProvider.
  • Critical/Danger toasts set role="alert" and require manual dismissal or a longer auto-dismiss duration.
  • Auto-dismiss timer automatically pauses on hover and focus, resuming when focus or pointer leaves.
  • Accessible dismiss button includes hidden screen-reader label (sr-only).

Code Example

import { useToast } from '../context/ToastContext'

export function BondActionButton() {
  const { addToast } = useToast()

  const handleCreateBond = async () => {
    try {
      await submitBondTransaction()
      addToast({
        severity: 'success',
        message: 'USDC bond successfully created.',
      })
    } catch (err) {
      addToast({
        severity: 'danger',
        message: err instanceof Error ? err.message : 'Transaction failed to broadcast.',
      })
    }
  }

  return (
    <button type="button" onClick={handleCreateBond} className="btn-primary">
      Create Bond
    </button>
  )
}

4. Page & Section Error States (ErrorState & ErrorBoundary)

Severity Tokens (canonical)

Severity Border / Surface tokens (light) Border / Surface tokens (dark) Use for
danger --credence-color-danger-border / -surface translucent --credence-color-danger-… (alpha) Blocking failures: lost connection, server crash, unhandled exception
warning --credence-color-warning-border / -surface translucent --credence-color-warning-… Recoverable: validation, slow path, scheduled maintenance
info --credence-color-info-border / -surface translucent --credence-color-info-… Non-blocking: cached fallback, 404, "no records" edge cases

The dark-mode block in src/index.css swaps these to translucent alpha values so the same component renders correctly under [data-theme='dark'] without per-component overrides.

Use Case

Use section or full-page error states when data failed to load completely or a component unhandled exception occurred, rendering content unusable.

Accessibility & Behavior

  • Renders role="alert" and aria-live="assertive" so screen readers announce the failure immediately.
  • The default title and message are derived from the type prop. Override via title / message for context-specific copy.
  • The CTA has min-height: 2.75rem (WCAG 2.5.5 target size) and a focus-visible ring tinted from --credence-color-primary.
  • A subtle entrance animation (translateY) is dropped entirely under prefers-reduced-motion: reduce.
  • Wrapped around route hierarchies via ErrorBoundary to prevent white-screen crashes.
  • All color tones come from design tokens (no hard-coded hex).

Severity Vocabulary

The severity prop is independent of the type prop. Use it to dial down the alarm. Defaults are derived from type:

type Default severity When to override
network danger Use warning for cached-fallback retrials
backend danger Use warning for scheduled maintenance windows
validation warning Use danger for auth/permission errors
generic danger Use info for gracefully degraded views
pageNotFound info Use danger if page is part of a critical flow

Copy Deck (default messages)

Calmer, non-alarming phrasing aligned with Copy Tone Guide:

type Title Message
network Connection issue We can't reach the service right now. Check your connection and try again in a moment.
backend Service temporarily unavailable We're hitting a brief snag on our end. Try again in a moment and we'll be back.
validation Check your input One or more fields need attention. Review the highlighted items and try again.
generic Something didn't load An unexpected hiccup stopped this view. Try again — if it persists, reach out and we'll help.
pageNotFound Page not found The page you're looking for doesn't exist. It may have moved or been renamed.

Iconography

Inline SVG glyphs using currentColor and aria-hidden="true". The accessible name comes from the surrounding title/message text. Never use emoji for error/sad-state visuals (see PR #936).

Code Example

import { useState, useEffect } from 'react'
import ErrorState from '../components/states/ErrorState'
import { apiFetch } from '../api/client'

export function AccountTrustPanel() {
  const [error, setError] = useState<Error | null>(null)
  const [data, setData] = useState(null)

  const loadData = async () => {
    setError(null)
    try {
      const res = await apiFetch('/account/trust-score')
      setData(res)
    } catch (err) {
      setError(err instanceof Error ? err : new Error('Failed to load trust score'))
    }
  }

  useEffect(() => {
    loadData()
  }, [])

  if (error) {
    return (
      <ErrorState
        type="network"
        title="Unable to load Trust Score"
        message={error.message}
        action={{
          label: 'Try Again',
          onClick: loadData,
        }}
      />
    )
  }

  return <div>{/* Render content */}</div>
}

Design Tokens & Styling Rules

Always use Credence design tokens for error surfaces. Never hardcode hex colors or pixel radii.

  • Danger Surface: var(--credence-color-danger-surface)
  • Danger Surface Strong: var(--credence-color-danger-surface-strong)
  • Danger Text: var(--credence-color-danger-text)
  • Danger Action Button: var(--credence-color-danger-action)
  • Border Radius: var(--credence-radius-lg), var(--credence-radius-xl)

For the full reference of available variables, consult Design Tokens.


Cross-References