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.
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 |
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).
- 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'ssuccessprop instead: it links${id}-successviaaria-describedby, usesrole="status", and does not setaria-invalid. If botherrorandsuccessare provided, error takes precedence.
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>
)
}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).
- Critical (
severity="critical") and warning (severity="warning") banners setrole="alert". - Information and success banners set
role="status". - Banners support inline actions (
action={{ label: 'Switch Network', onClick: handleSwitch }}). - Dismissible banners support focus restoration via
returnFocusRefand keyboard escape (Escapekey).
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>
)
}Use toasts for short, non-blocking feedback following an asynchronous user action (e.g., wallet submission failed, link copied to clipboard, or settings saved).
- 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).
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>
)
}| 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 section or full-page error states when data failed to load completely or a component unhandled exception occurred, rendering content unusable.
- Renders
role="alert"andaria-live="assertive"so screen readers announce the failure immediately. - The default title and message are derived from the
typeprop. Override viatitle/messagefor context-specific copy. - The CTA has
min-height: 2.75rem(WCAG 2.5.5 target size) and afocus-visiblering tinted from--credence-color-primary. - A subtle entrance animation (translateY) is dropped entirely under
prefers-reduced-motion: reduce. - Wrapped around route hierarchies via
ErrorBoundaryto prevent white-screen crashes. - All color tones come from design tokens (no hard-coded hex).
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 |
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. |
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).
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>
}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.
- UI States Guide — Loading, empty, and general error state rules.
- Copy Tone Guide — Voice, tone, and microcopy guidelines for error messages.
- Shared Components Catalog — Component props reference for
Banner,Toast, andFormField. - Form Inputs & Variants — Input state contracts and validation patterns.
- Design Tokens — Token variables reference for CSS styling.