should support this action, not compete for attention. The layout should visually prioritize the primary interaction while grouping supporting data.
import React from 'react';
interface PrimaryActionLayoutProps {
primaryAction: React.ReactNode;
supportingMetrics: Array<{ label: string; value: string }>;
contextHint?: string;
}
export const PrimaryActionLayout: React.FC<PrimaryActionLayoutProps> = ({
primaryAction,
supportingMetrics,
contextHint,
}) => {
return (
<div className="flex flex-col gap-6">
{/* Primary Action gets visual dominance */}
<section className="p-6 bg-surface-elevated rounded-xl border border-border-subtle">
{primaryAction}
</section>
{/* Supporting metrics are grouped and de-emphasized */}
<section className="grid grid-cols-2 gap-4">
{supportingMetrics.map((metric) => (
<div key={metric.label} className="flex flex-col">
<span className="text-text-muted text-sm">{metric.label}</span>
<span className="text-text-primary font-medium">{metric.value}</span>
</div>
))}
</section>
{/* Context hint provides immediate "so what?" */}
{contextHint && (
<p className="text-text-secondary text-xs mt-2">{contextHint}</p>
)}
</div>
);
};
Rationale: This component structure forces developers to separate the primary interaction from supporting data. The contextHint prop ensures that raw numbers are accompanied by interpretive text, addressing the need for immediate context.
2. Semantic Color Systems
Color must convey meaning consistently. Deviating from established conventions for brand aesthetics compromises usability. Green/red for trends and status colors must be enforced programmatically.
// tokens.ts
export const SEMANTIC_COLORS = {
TREND_UP: 'text-green-500',
TREND_DOWN: 'text-red-500',
STATUS_ACTIVE: 'bg-green-500',
STATUS_WARNING: 'bg-yellow-500',
STATUS_ERROR: 'bg-red-500',
} as const;
// useThemeColors.ts
export const useThemeColors = () => {
const getTrendColor = (isPositive: boolean) =>
isPositive ? SEMANTIC_COLORS.TREND_UP : SEMANTIC_COLORS.TREND_DOWN;
const getStatusColor = (status: 'active' | 'warning' | 'error') =>
SEMANTIC_COLORS[`STATUS_${status.toUpperCase()}` as const];
return { getTrendColor, getStatusColor };
};
Rationale: By centralizing color definitions, the application prevents accidental overrides. The useThemeColors hook ensures that trend and status colors are applied based on data state, maintaining consistency across the interface.
3. Contextual Number Formatting
Numbers require formatting that matches human reading patterns. Precision should be contextual: gas costs need decimals; TVL does not. Raw values must be augmented with context.
// formatters.ts
export const formatFinancialValue = (
value: number,
options: {
precision?: number;
suffix?: 'M' | 'K' | 'B';
showContext?: boolean;
change?: number;
} = {}
): string => {
const { precision = 2, suffix, showContext, change } = options;
let formatted = value.toLocaleString('en-US', {
minimumFractionDigits: precision,
maximumFractionDigits: precision,
});
if (suffix) {
// Simplified suffix logic for example
formatted = `${(value / 1e6).toFixed(1)}${suffix}`;
}
if (showContext && change !== undefined) {
const changeStr = change >= 0 ? `+${change}%` : `${change}%`;
return `${formatted} (${changeStr})`;
}
return formatted;
};
Rationale: This formatter handles human-readable scaling and appends relative change data. It prevents "precision paralysis" by allowing callers to specify appropriate precision based on the metric type.
4. Proximity-Based Trust Signals
Trust indicators must be placed near the actions they validate. A badge near an "Approve" button is functional; one in the footer is decorative.
import { ShieldCheck } from 'lucide-react';
interface TrustBadgeProps {
auditStatus: 'verified' | 'pending' | 'unverified';
contractAddress: string;
placement: 'inline' | 'modal' | 'footer';
}
export const TrustBadge: React.FC<TrustBadgeProps> = ({
auditStatus,
contractAddress,
placement
}) => {
if (placement === 'footer') return null; // Discourage footer usage
return (
<div className={`flex items-center gap-2 text-xs ${
auditStatus === 'verified' ? 'text-green-600' : 'text-red-600'
}`}>
<ShieldCheck size={14} />
<span>
{auditStatus === 'verified' ? 'Audited' : 'Audit Pending'}
</span>
{placement === 'inline' && (
<code className="bg-surface-muted px-1 rounded">
{contractAddress.slice(0, 6)}...{contractAddress.slice(-4)}
</code>
)}
</div>
);
};
Rationale: The component explicitly discourages footer placement. When placement is inline, it renders the contract address snippet, providing immediate verification capability next to the transaction trigger.
5. Latency-Aware State Management
Blockchain transactions are asynchronous. Interfaces must reflect progress, provide ETAs, and allow users to continue working while waiting. Spinners are insufficient.
interface TransactionStep {
id: string;
label: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
eta?: number; // seconds
}
interface TransactionFlowProps {
steps: TransactionStep[];
onCancel: () => void;
}
export const TransactionFlow: React.FC<TransactionFlowProps> = ({ steps, onCancel }) => {
const currentStep = steps.find(s => s.status === 'processing');
return (
<div className="space-y-4">
<h3 className="font-semibold">Transaction Progress</h3>
<ul className="space-y-2">
{steps.map(step => (
<li key={step.id} className="flex items-center gap-3">
<span className={`w-2 h-2 rounded-full ${
step.status === 'completed' ? 'bg-green-500' :
step.status === 'processing' ? 'bg-blue-500 animate-pulse' :
'bg-gray-300'
}`} />
<span className="text-sm">{step.label}</span>
{step.status === 'processing' && step.eta && (
<span className="text-xs text-text-muted">~{step.eta}s</span>
)}
</li>
))}
</ul>
<button
onClick={onCancel}
className="text-sm text-text-muted hover:text-text-primary"
>
Cancel Transaction
</button>
</div>
);
};
Rationale: This component replaces generic spinners with a step-by-step progress indicator. It displays ETAs and allows cancellation, giving users control and visibility during latency periods.
Pitfall Guide
-
The Data Dump Trap
- Explanation: Displaying all available metrics (APY, TVL, 7d change, gas, rewards) simultaneously creates visual noise and obscures the primary action.
- Fix: Implement strict hierarchy. Define one primary action per screen and group secondary metrics in a de-emphasized section.
-
Aesthetic Color Overrides
- Explanation: Using brand colors for trends or status indicators (e.g., red for profit) breaks user mental models and causes misinterpretation.
- Fix: Enforce semantic color tokens. Brand colors should never override trend or status definitions.
-
Precision Paralysis
- Explanation: Showing excessive decimals for large values (e.g., TVL with 8 decimal places) adds no value and increases cognitive load.
- Fix: Apply contextual precision rules. Use suffixes (M, K, B) for large values and reserve decimals for high-precision needs like gas costs.
-
Distant Trust Indicators
- Explanation: Placing audit badges or security warnings in footers or modals separates trust signals from the actions they validate.
- Fix: Render trust badges inline with critical actions. Ensure contract addresses are visible near approval buttons.
-
Opaque Pending States
- Explanation: Using a simple spinner during transaction confirmation provides no feedback on progress or expected duration, leading to user anxiety.
- Fix: Implement progress flows with step indicators and ETAs. Allow users to navigate away or perform other actions while waiting.
-
Static Loading Blocks
- Explanation: Blocking the entire UI during data fetches prevents users from interacting with available content.
- Fix: Use skeleton loaders for specific sections. Allow interaction with non-dependent UI elements while data loads.
-
Missing Contextual Benchmarks
- Explanation: Displaying a value like "$1.2M" without context leaves users unable to assess performance or risk.
- Fix: Always pair key metrics with relative changes, sparklines, or benchmarks to answer "so what?" within 200ms.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Staking Dashboard | Action-Centric | High risk, clear goal requires focus | Low |
| Portfolio Overview | Data-Dense with Context | Users need scanability of multiple assets | Medium |
| New User Onboarding | Guided Flow | Low trust, high confusion requires hand-holding | High |
| Advanced Trading | Hierarchical with Toggle | Experts need density but beginners need simplicity | Medium |
| Emergency Withdrawal | Trust-First | Maximum risk requires immediate verification | Low |
Configuration Template
// config/ui.ts
export const UI_CONFIG = {
hierarchy: {
maxPrimaryActions: 1,
secondaryMetricsGroup: true,
},
colors: {
enforceSemantic: true,
brandOverrides: false,
},
formatting: {
defaultPrecision: 2,
largeValueSuffix: true,
contextRequired: true,
},
trust: {
badgePlacement: 'inline',
showContractAddress: true,
},
latency: {
useProgressFlow: true,
showETA: true,
allowCancellation: true,
},
};
Quick Start Guide
- Initialize Theme Tokens: Set up semantic color tokens and enforce them in your design system.
- Wrap Metrics: Replace raw number displays with
formatFinancialValue calls, specifying precision and context.
- Add Trust Badges: Insert
TrustBadge components inline with approval buttons, passing placement="inline".
- Implement Progress Flows: Swap loading spinners for
TransactionFlow components in all transaction handlers.
- Validate Hierarchy: Review each screen to ensure only one primary action is visually dominant. Group secondary data.