This guide defines the design patterns, visual treatments, and implementation details for user interface states in the RemitWise platform. It is written for frontend contributors to ensure a consistent experience across form submissions, dashboard widgets, and data loading boundaries.
The default state represents components in their idle, interactive form. Design guidelines dictate that interactive elements must have clear visual cues for focus, hover, and active states.
All styling utilizes Tailwind CSS and respects our global design tokens configured in tailwind.config.js. Avoid hardcoding hex colors, border radii, or spacing values.
Key interactive tokens:
- Brand Accent:
bg-brand.red(#DC2626) - Hover Accent:
bg-brand.redHover(#B91C1C) - Focus Rings: Always use outline offsets and clear rings.
Here is the implementation of a standard input field using our Tailwind design tokens:
import React from 'react';
export function TextInput({
label,
name,
placeholder,
defaultValue,
}: {
label: string;
name: string;
placeholder?: string;
defaultValue?: string;
}) {
return (
<div className="grid gap-1.5">
<label
htmlFor={name}
className="block text-sm font-medium text-gray-300"
>
{label}
</label>
<input
type="text"
id={name}
name={name}
defaultValue={defaultValue}
placeholder={placeholder}
className="w-full px-4 py-3 bg-[#0A0A0A] border border-white/10 rounded-lg text-white placeholder-gray-500 transition-colors duration-200 hover:border-white/20 focus:outline-none focus:ring-2 focus:ring-brand.red focus:border-transparent focus:ring-offset-2 focus:ring-offset-black"
/>
</div>
);
}To prevent layout shifts and provide a premium user experience, RemitWise uses route-level skeleton screens instead of generic spinners. Inline loading spinners are reserved for form submissions on buttons.
Located in components/ui/Skeleton.tsx, the Skeleton components animate using a shimmer effect.
We support three primary layout skeletons:
SkeletonCard: Standard placeholder block. Variants include"default","stat", and"chart".SkeletonList: List layout wrapper. Variants include"table"and"cards".DashboardLoadingSkeleton: High-level dashboard shell.
import { SkeletonCard } from "@/components/ui/Skeleton";
export function WidgetLoading() {
return (
<div className="space-y-4">
<h3 className="text-white font-medium">Analytics Preview</h3>
{/* Renders a stat card placeholder with animated shimmer */}
<SkeletonCard variant="stat" />
</div>
);
}When submitting forms, the action button should display a loading spinner and transition text while disabling interactions. Use a shared loader icon, expose aria-busy for the busy state, and keep the action disabled until the request completes.
import { Loader2 } from 'lucide-react';
export function SubmitButton({ pending }: { pending: boolean }) {
return (
<button
type="submit"
disabled={pending}
className="flex items-center justify-center w-full bg-brand.red hover:bg-brand.redHover text-white px-6 py-3 rounded-lg font-semibold transition disabled:opacity-50"
>
{pending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Processing...
</>
) : (
"Confirm Transfer"
)}
</button>
);
}Interactive controls are placed in a disabled state for two reasons:
- In-Flight Requests: Form inputs and submit buttons must be disabled during active submissions to prevent duplicate form submissions or double-spends.
- Feature Boundaries: Features waiting for integration with USDC smart contracts or external providers disable fields to guide the user (e.g.
NewPolicyForm.tsx).
- Apply
disabled:opacity-50anddisabled:cursor-not-allowed. - Text color is muted (
text-gray-500ortext-white/30). - Borders are softened (
border-white/5orborder-gray-200).
import React from 'react';
export function DisabledField({ label, value }: { label: string; value: string }) {
return (
<div className="grid gap-1">
<label className="block text-sm font-medium text-gray-400">{label}</label>
<input
type="text"
value={value}
disabled
className="w-full px-4 py-3 border border-white/5 bg-white/[0.02] rounded-lg text-white/50 cursor-not-allowed opacity-50 focus:outline-none"
/>
</div>
);
}RemitWise handles errors at two levels: form validation/API errors and component/widget render errors.
Form submissions utilize the useFormAction hook. The hook handles error resolution priority and returns errors within the state object.
import { useFormAction } from '@/lib/hooks/useFormAction';
export function SendForm() {
const [state, formAction, isPending] = useFormAction('/api/send');
return (
<form action={formAction} className="space-y-4">
{/* Standard error banner using semantic red color tokens */}
{state?.error && (
<div className="p-3 bg-status-error-soft border border-status-error-border rounded-lg text-status-error-fg text-sm">
{state.error}
</div>
)}
{/* Inputs disabled during submission */}
<input
type="number"
name="amount"
disabled={isPending}
className="border border-white/10 bg-black text-white p-2 rounded"
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Sending...' : 'Send'}
</button>
</form>
);
}If an individual widget fails during rendering, a reusable WidgetErrorBoundary catches the failure, logs the incident via the server logging service, and renders WidgetErrorState without crashing the rest of the application.
- Boundary Component:
components/ui/WidgetErrorBoundary.tsx - Fallback State UI:
components/ui/WidgetErrorState.tsx
import WidgetErrorBoundary from '@/components/ui/WidgetErrorBoundary';
import MyWidgetComponent from './MyWidgetComponent';
export function DashboardLayout() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Wrap widgets individually to isolate errors */}
<WidgetErrorBoundary widgetName="MyWidgetComponent">
<MyWidgetComponent />
</WidgetErrorBoundary>
</div>
);
}For deep dives into related patterns, see:
- Error Handling Strategy — Covers global error boundaries and logger configurations.
- Form Action Hook Guide — Explains state transitions during AJAX form requests.
- Client API Guide — Explains
apiClientrequests, retry delays, and session expiry flows. - Status Semantics Handoff — Visual design specifications for semantic statuses.