Skip to content

Loading States

Loading states provide visual feedback while data is being fetched or actions are processing. This guide covers common patterns using @f0rbit/ui components.

Use the loading prop on buttons to indicate an action is in progress. This disables interaction and shows a spinner.

import { createSignal } from "solid-js";
import { Button } from "@f0rbit/ui";
function SaveButton() {
const [loading, setLoading] = createSignal(false);
const handleSave = async () => {
setLoading(true);
await saveData();
setLoading(false);
};
return (
<Button loading={loading()} onClick={handleSave}>
{loading() ? "Saving..." : "Save Changes"}
</Button>
);
}
  • Change button text to reflect the action (“Save” → “Saving…”)
  • Disable form inputs while submitting to prevent duplicate submissions
  • Keep the button width consistent to avoid layout shift
<Button loading={loading()} style={{ "min-width": "120px" }}>
{loading() ? "Saving..." : "Save"}
</Button>

For initial page loads or major data fetching, center a spinner on the page:

import { Spinner } from "@f0rbit/ui";
function PageLoader() {
return (
<div class="page-loader">
<Spinner size="lg" />
<span>Loading...</span>
</div>
);
}
.page-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--space-md);
min-height: 400px;
color: var(--color-text-secondary);
}
import { Show } from "solid-js";
function DataPage() {
const [data] = createResource(fetchData);
return (
<Show when={!data.loading} fallback={<PageLoader />}>
<DataContent data={data()} />
</Show>
);
}

For smaller areas, use a small spinner inline with text:

import { Spinner } from "@f0rbit/ui";
function LoadingMessage() {
return (
<span style={{ display: "flex", "align-items": "center", gap: "0.5rem" }}>
<Spinner size="sm" />
Loading items...
</span>
);
}

While @f0rbit/ui doesn’t include a dedicated skeleton component, you can create skeleton placeholders using CSS:

function CardSkeleton() {
return (
<div class="skeleton-card">
<div class="skeleton skeleton-title" />
<div class="skeleton skeleton-text" />
<div class="skeleton skeleton-text short" />
</div>
);
}
.skeleton {
background: linear-gradient(
90deg,
var(--color-bg-secondary) 25%,
var(--color-bg-tertiary) 50%,
var(--color-bg-secondary) 75%
);
background-size: 200% 100%;
animation: skeleton-pulse 1.5s ease-in-out infinite;
border-radius: var(--radius-sm);
}
.skeleton-title {
height: 1.5rem;
width: 60%;
margin-bottom: var(--space-sm);
}
.skeleton-text {
height: 1rem;
width: 100%;
margin-bottom: var(--space-xs);
}
.skeleton-text.short {
width: 40%;
}
@keyframes skeleton-pulse {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}

Combine with the Card component structure:

import { Card, CardHeader, CardContent } from "@f0rbit/ui";
function CardListSkeleton(props: { count: number }) {
return (
<div class="stack">
{Array.from({ length: props.count }).map(() => (
<Card>
<CardHeader>
<div class="skeleton skeleton-title" />
</CardHeader>
<CardContent>
<div class="skeleton skeleton-text" />
<div class="skeleton skeleton-text" />
<div class="skeleton skeleton-text short" />
</CardContent>
</Card>
))}
</div>
);
}

For complex UIs, combine multiple loading patterns:

import { createSignal, Show } from "solid-js";
import { Button, Spinner, Card, CardContent } from "@f0rbit/ui";
function DataPanel() {
const [data, { refetch }] = createResource(fetchData);
const [refreshing, setRefreshing] = createSignal(false);
const handleRefresh = async () => {
setRefreshing(true);
await refetch();
setRefreshing(false);
};
return (
<Card>
<CardContent>
<div class="panel-header">
<h3>Data Panel</h3>
<Button
variant="ghost"
size="sm"
loading={refreshing()}
onClick={handleRefresh}
>
Refresh
</Button>
</div>
<Show
when={!data.loading}
fallback={
<div class="panel-loading">
<Spinner />
</div>
}
>
<DataList items={data()} />
</Show>
</CardContent>
</Card>
);
}
  1. Provide feedback immediately - Show loading state as soon as an action starts
  2. Use appropriate sizes - Small spinner for buttons, large for page loading
  3. Maintain layout stability - Reserve space for content to avoid layout shift
  4. Consider perceived performance - Skeleton loading often feels faster than spinners
  5. Add timeouts for long operations - Show additional messaging for operations taking more than a few seconds
function LongOperationButton() {
const [loading, setLoading] = createSignal(false);
const [longWait, setLongWait] = createSignal(false);
const handleClick = async () => {
setLoading(true);
const timeout = setTimeout(() => setLongWait(true), 3000);
await longOperation();
clearTimeout(timeout);
setLoading(false);
setLongWait(false);
};
return (
<div class="stack">
<Button loading={loading()} onClick={handleClick}>
Process Data
</Button>
<Show when={longWait()}>
<span class="text-secondary text-sm">
This is taking longer than expected...
</span>
</Show>
</div>
);
}