This guide defines the empty states, error states, and loading patterns for the Credence Frontend application. These states ensure a consistent, helpful user experience across all views.
Empty states appear when there's no data to display. They should be encouraging and guide users toward their next action.
- Clear & Concise: Use simple language that explains why the state is empty
- Actionable: Provide a clear next step when possible
- Visual: Include relevant icons or illustrations
- Tone: Friendly, encouraging, never blaming the user
When: User has not created any bond Location: Bond page, Home highlights
<EmptyState
illustration="bond"
title="No bond created yet"
description="Lock USDC into Credence to build your economic reputation and unlock trust-based opportunities."
action={{
label: 'Create your first bond',
onClick: () => navigate('/bond'),
}}
/>Microcopy Guidelines:
- Title: 4-6 words, state the fact
- Description: 1-2 sentences, explain the benefit
- CTA: Action-oriented verb + outcome
This section defines the first-run help pattern for the Bond page. The goal is to guide new users through their first bond creation without interrupting expert users.
- Show a dismissible coach mark on first Bond page visit when the user has no existing bond.
- Anchor the coach mark to the bond form area and primary CTA, not as a modal overlay.
- Provide a complementary inline helper text in the form header or description so the experience is accessible and lightweight.
- Avoid blocking expert users: the coach mark is optional and can be closed immediately.
- Title: "Create your first bond"
- Description: "Lock USDC into Credence to build your economic reputation. Start with a small amount and your bond will show up in your dashboard."
- Primary action: "Got it" or "Start bond"
- Secondary action: "Not now" or close icon
- Optional inline helper: "Bonds are locked for a minimum of 30 days; early withdrawal may incur a penalty."
- The coach mark must be reachable by keyboard and screen readers.
- It must not trap focus. Focus should remain in the main form flow or move naturally to the close button.
- Use an accessible label and descriptive text, e.g.
aria-describedby="bond-onboarding-desc". - Support dismissal via keyboard (
Tab,Enter,Space,Escape). - If the coach mark is visible, the page remains fully navigable and the user can interact with the form behind it.
- Allow users to dismiss the coach mark with a close button or primary acknowledgment action.
- Persist dismissal state only after legal/privacy review. For design purposes, the recommended implementation is localStorage with a namespaced key such as
credence.bondOnboardingDismissed. - Do not show the coach mark again after dismissal, unless the user explicitly resets onboarding in settings or clears site data.
- Do not persist dismissal for authenticated preferences unless privacy requirements allow it.
- First time a wallet user lands on
/bondand has no active bonds recorded. - Do not show on subsequent visits after dismissal.
- Do not show after the user has already created a bond.
- On page refresh, if the coach mark was previously dismissed, keep it hidden.
- Prefer a non-modal toast-like coach mark positioned near the bond amount field and create button.
- If a static prototype is desired, use a simple page overlay callout on the Bond page in the design doc rather than implementing runtime behavior.
- Keep the bond form usable while the coach mark is visible.
When: Address has no trust score data Location: Trust Score page
<EmptyState
illustration="trust"
title="No trust score found"
description="This address hasn't established a trust score yet. Create a bond and gather attestations to build reputation."
action={{
label: 'Learn how trust scores work',
onClick: () => window.open('/docs/trust-score', '_blank'),
variant: 'secondary',
}}
/>When: No disputes exist for the user Location: Activity/Governance page
<EmptyState
illustration="dispute"
title="No disputes"
description="You have no active or past disputes. Your reputation remains intact."
/>Note: No action needed - this is a positive state
When: User has no attestations Location: Trust Score details, Profile
<EmptyState
illustration="attestation"
title="No attestations yet"
description="Attestations from trusted parties strengthen your reputation. Complete transactions and request attestations to build trust."
action={{
label: 'Request attestation',
onClick: () => navigate('/attestations/request'),
}}
/>When: No governance or activity history Location: Activity/Governance page
<EmptyState
illustration="activity"
title="No activity yet"
description="Your governance participation and transaction history will appear here."
/>Error states inform users when something goes wrong and help them recover.
- Honest: Clearly state what went wrong
- Helpful: Explain what the user can do
- Non-technical: Avoid jargon and error codes in primary message
- Tone: Apologetic but solution-focused
When: Unable to connect to Stellar network or backend Trigger: Network timeout, offline, DNS failure
<ErrorState
type="network"
action={{
label: 'Try again',
onClick: () => refetch(),
}}
/>Default Message: "Unable to connect to the network. Check your internet connection and try again."
When: API returns 500, service unavailable Trigger: Server error, maintenance
<ErrorState
type="backend"
title="Service temporarily unavailable"
message="We're experiencing technical difficulties. Please try again in a few moments."
action={{
label: 'Retry',
onClick: () => refetch(),
}}
/>When: User enters malformed Stellar address Trigger: Validation failure on trust score lookup
<ErrorState
type="validation"
title="Invalid wallet address"
message="Please enter a valid Stellar address starting with 'G' and containing 56 characters."
action={{
label: 'Go back',
onClick: () => reset(),
}}
/>When: Blockchain transaction fails Trigger: Insufficient funds, rejected transaction
<ErrorState
type="generic"
title="Transaction failed"
message="Your transaction could not be completed. Please check your wallet balance and try again."
action={{
label: 'Try again',
onClick: () => retryTransaction(),
}}
/>Loading states provide feedback during asynchronous operations.
- Immediate: Show loading state instantly
- Contextual: Match the content being loaded
- Smooth: Use subtle animations
- Consistent: Same patterns across the app
When: Submitting bond creation, attestation request Usage:
{
isLoading ? <LoadingSkeleton variant="form" rows={3} /> : <BondForm />
}When: Loading home highlights, stats cards Usage:
{
isLoading ? <LoadingSkeleton variant="dashboard" rows={3} /> : <DashboardCards />
}When: Loading activity history, dispute list Usage:
{
isLoading ? <LoadingSkeleton variant="table" rows={5} /> : <ActivityTable />
}When: Loading descriptions, details Usage:
{
isLoading ? <LoadingSkeleton variant="text" rows={3} /> : <TrustScoreDetails />
}-
State Priority: Check states in this order:
- Loading → Error → Empty → Content
-
Composition Pattern:
function MyComponent() {
const { data, isLoading, error } = useQuery()
if (isLoading) return <LoadingSkeleton variant="card" />
if (error) return <ErrorState type="network" />
if (!data || data.length === 0) return <EmptyState {...emptyConfig} />
return <Content data={data} />
}-
Accessibility:
- Loading states should have
role="status"andaria-live="polite" - Error states should have
role="alert" - Empty states should be keyboard navigable if they have actions
- Loading states should have
-
Testing:
- Test each state independently
- Verify transitions between states
- Check responsive behavior
- Friendly: Use conversational language
- Clear: Avoid ambiguity
- Concise: Respect user's time
- Helpful: Always suggest next steps
- Titles: 3-6 words
- Descriptions: 1-2 sentences (max 140 characters)
- CTAs: 2-4 words, action verb + object
✅ Good:
- "No bonds yet" / "Create your first bond"
- "Connection lost" / "Check your network and retry"
❌ Avoid:
- "You haven't created any bonds in the system yet" (too wordy)
- "Error 500" (too technical)
- "Oops! Something went wrong!" (overused, not helpful)
Empty States:
- Background: Contextual (blue for bond, purple for trust, etc.)
- Text: #0f172a (title), #64748b (description)
Error States:
- Background: #fef2f2
- Border: #fee2e2
- Text: #991b1b (title), #7f1d1d (description)
- Button: #dc2626
Loading States:
- Base: #f1f5f9
- Shimmer: #e2e8f0
- Icon/Illustration: 64px diameter
- Icon margin-bottom: 16px
- Title margin-bottom: 8px
- Description margin-bottom: 24px (if action present)
- Padding: 48px 24px
- Title: 18px, font-weight 600
- Description: 14px, line-height 1.5
- Button: 14px, font-weight 600
Before shipping, validate:
- All empty states have clear CTAs (where appropriate)
- Error messages are user-friendly (no technical jargon)
- Loading skeletons match content layout
- All states are responsive (mobile, tablet, desktop)
- Microcopy follows tone guidelines
- Accessibility attributes are present
- States transition smoothly
- Product team has approved CTAs and messaging
Two boundaries protect the app from white-screen crashes:
| Layer | Component | Location | Catches |
|---|---|---|---|
| Root (outermost) | ErrorBoundary |
main.tsx wrapping <App /> |
Provider bootstrap errors |
| Route tree | ErrorBoundary |
App.tsx wrapping <Suspense> |
Lazy-chunk failures, route render errors |
| Router-level | RouteErrorPage |
errorElement on root <Route> |
Loader errors (data-router) |
import ErrorBoundary from './components/ErrorBoundary'
// Default fallback (ErrorState + retry + home link)
<ErrorBoundary>
<SomeSubtree />
</ErrorBoundary>
// Custom fallback via render prop
<ErrorBoundary fallback={(error, reset) => (
<div>
<p>{error.message}</p>
<button onClick={reset}>Retry</button>
</div>
)}>
<SomeSubtree />
</ErrorBoundary>Retry behaviour: clicking "Try again" calls setState({ hasError: false, error: null }).
React re-renders the children without a hard reload. If they throw again the boundary
catches once more.
Telemetry hook: componentDidCatch currently logs to console.error. Replace the
console.error call with your error monitoring SDK (Sentry, Datadog, etc.) before shipping.
Registered as errorElement on the root route. In the current BrowserRouter setup this
serves as forward-compatible scaffolding; it becomes fully active if the app is migrated to
createBrowserRouter. Handles 404 (route miss) and generic router errors with the same
ErrorState visual.
- The fallback renders an
<h3>heading fromErrorState— screen readers announce the error. - A
<a href="/">Go to home page</a>link ensures keyboard-only users have a navigation escape hatch even if JavaScript is broken. - The retry
<button>isfocus-visiblestyled via the shared class.
Each bond row in the Active Bonds list surfaces penalty exposure inline — before the user commits to withdrawing — via an accessible disclosure.
| Bond status | Disclosure control | Panel content |
|---|---|---|
locked |
"Show penalty" button (aria-expanded) |
Bond amount, 20% penalty amount, resulting balance |
grace-period |
"Show penalty" button (aria-expanded) |
Bond amount, 10% penalty amount, resulting balance |
active |
None (no expander) | Static "No early-withdrawal penalty" message |
- The toggle button carries
aria-expanded="false"when collapsed andaria-expanded="true"when open, pointing to the panel viaaria-controls. - The panel uses
role="region"with anaria-labelidentifying the bond. - The breakdown values (bond amount, penalty %, penalty USDC, resulting balance) are computed by
computeWithdrawBreakdown— the same function used in theConfirmDialog— so numbers are identical at both stages. - Active bonds show a "No early-withdrawal penalty" message in place of the expander to make the safe state explicit.
- The row layout uses
flexWrap: wrapso it reflows cleanly on narrow viewports.
The existing page-level slash-exposure Banner remains above the bond list as a summary callout. The per-row disclosure is the detailed, bond-specific complement to that banner — not a replacement.
- Custom illustrations for each empty state
- Animated loading states
- Progressive loading (show partial content)
- Contextual help links in error states
- A/B test different microcopy variations