Components

Built once, rendered everywhere

Every component ships as a typed React export and the plain CSS class it renders — so a Hugo partial and a React page produce pixel-identical UI. Demos below are live and theme-aware; flip the theme toggle to check dark mode.

React + CSS parity Stable class names
Actions

Buttons

Variants & sizes

One primary action per view. ghost for tertiary actions, semantic variants for their outcomes.

import { Button } from '@readysetcloud/ui';

<Button variant="primary">Primary</Button>
<Button variant="ghost" size="sm">Ghost</Button>
<Button loading loadingLabel="Saving…">Save</Button>
<button class="btn btn-primary">Primary</button>
<button class="btn btn-ghost btn-sm">Ghost</button>
<button class="btn btn-primary" disabled>
  <span class="spinner"></span> Saving…
</button>
Status

Badges & status

Badge

The uppercase mono tag — for categorical labels like tiers, environments, and content types.

Primary Success Warning Error Neutral
import { Badge } from '@readysetcloud/ui';

<Badge variant="success">Success</Badge>
<span class="badge badge-success">Success</span>

StatusBadge

The sentence-case health pill — for state readouts and threshold flags. One tone class is correct in both themes; the ramps invert for you.

Stable High Critical Scheduled Draft
import { StatusBadge } from '@readysetcloud/ui';

<StatusBadge tone="success" icon="✓">Stable</StatusBadge>
<StatusBadge tone="warning" icon="⚠">High</StatusBadge>
<span class="status-badge status-badge-success" role="status">
  <span aria-hidden="true">✓</span> Stable
</span>
Data display

Analytics kit

The shared language of the dashboards: metric tiles with delta pills, threshold badges, and trend sparklines, plus the segmented control that switches their baseline.

TrendPill

Color encodes improvement, not direction — pass invert for metrics where down is good (bounce, unsubscribes). Zero deltas render neutral with no arrow.

+6.3% -2.1% -0.4% (bounce ↓ = good) No change
import { TrendPill } from '@readysetcloud/ui';

<TrendPill delta={6.3} />            // improvement → success colors
<TrendPill delta={-2.1} />           // decline → error colors
<TrendPill delta={-0.4} invert />    // bounce rate fell → improvement
<TrendPill delta={0} />              // neutral, no arrow
<span class="trend-pill trend-pill-positive">
  <svg viewBox="0 0 16 16" aria-hidden="true">
    <path d="M3 10.5 8 5.5 13 10.5" fill="none"
          stroke="currentColor" stroke-width="2"
          stroke-linecap="round" stroke-linejoin="round"/>
  </svg>
  +6.3%
</span>

SegmentedControl

Compact multi-option toggle for comparison baselines, view switches, and filter tabs. State is exposed via aria-pressed. Try it — this one is live.

Compare to
import { SegmentedControl } from '@readysetcloud/ui';

<SegmentedControl
  aria-label="Comparison baseline"
  options={[
    { value: 'average', label: 'Average' },
    { value: 'last', label: 'Last issue' },
    { value: 'best', label: 'Best issue' }
  ]}
  value={mode}
  onChange={setMode}
/>
<div class="segmented-control" role="group" aria-label="Comparison baseline">
  <button class="segmented-control-option" aria-pressed="true">Average</button>
  <button class="segmented-control-option" aria-pressed="false">Last issue</button>
  <button class="segmented-control-option" aria-pressed="false">Best issue</button>
</div>

StatTile + TrendSparkline

The dashboard metric tile: uppercase label, display-font value, delta pill, threshold badge, caption, and a decorative sparkline with the latest point emphasized. Sparklines are aria-hidden — the numbers carry the meaning.

Open Rate
48.0% +6.3%
1,234 opens · vs. avg
Bounce Rate
6.2% High
67 bounced · vs. avg
Subscribers
2,568
+41 this week
import { StatTile } from '@readysetcloud/ui';

<StatTile
  label="Open Rate"
  value="48.0%"
  delta={6.3}
  meta="1,234 opens · vs. avg"
  sparkline={[38, 42, 40, 45, 43, 48]}
/>

<StatTile
  label="Bounce Rate"
  value="6.2%"
  delta={-0.4}
  invertDelta               // falling bounce = improvement
  status={{ tone: 'warning', label: 'High' }}
  meta="67 bounced · vs. avg"
  sparkline={[3.1, 3.4, 4.2, 4.0, 5.1, 6.2]}
/>
<div class="stat-tile">
  <div class="stat-tile-header">
    <div>
      <div class="stat-tile-label">Open Rate</div>
      <div class="stat-tile-row">
        <span class="stat-tile-value">48.0%</span>
        <span class="trend-pill trend-pill-positive">+6.3%</span>
      </div>
    </div>
  </div>
  <div class="stat-tile-meta">1,234 opens · vs. avg</div>
  <div class="stat-tile-footer"><div id="opens-trend"></div></div>
</div>
<script>
  // window.rscUi from ui.global.js
  rscUi.renderSparkline(
    document.getElementById('opens-trend'),
    [38, 42, 40, 45, 43, 48]
  );
</script>
Layout

Page hero

PageHero

The gradient band that opens a page: surface-to-primary wash, blurred accent blob (drawn in CSS — no extra markup), display-font title, and pill meta chips.

Serverless Picks of the Week #204

Sent to your loyal readers, tracked end to end.

Issue #204 Created Jul 18 Sent Jul 21 2,568 recipients
import {
  PageHero, PageHeroTitle, PageHeroSubtitle, PageHeroChips, PageHeroChip
} from '@readysetcloud/ui';

<PageHero>
  <PageHeroTitle>Serverless Picks of the Week #204</PageHeroTitle>
  <PageHeroSubtitle>Sent to your loyal readers, tracked end to end.</PageHeroSubtitle>
  <PageHeroChips>
    <PageHeroChip>Issue #204</PageHeroChip>
    <PageHeroChip tone="success">Sent Jul 21</PageHeroChip>
  </PageHeroChips>
</PageHero>
<header class="page-hero">
  <div class="page-hero-content">
    <h1 class="page-hero-title">Serverless Picks of the Week #204</h1>
    <p class="page-hero-subtitle">Sent to your loyal readers.</p>
    <div class="page-hero-chips">
      <span class="page-hero-chip">Issue #204</span>
      <span class="page-hero-chip page-hero-chip-success">Sent Jul 21</span>
    </div>
  </div>
</header>
Layout

Cards

Card

The compound container behind most content: header, title, description, body, footer.

Sending health

Deliverability signals for the last 10 issues.

Bounce, complaint, and unsubscribe rates are all inside their thresholds.
import { Card, CardHeader, CardTitle, CardDescription, CardBody, CardFooter } from '@readysetcloud/ui';

<Card>
  <CardHeader>
    <CardTitle>Sending health</CardTitle>
    <CardDescription>Deliverability signals.</CardDescription>
  </CardHeader>
  <CardBody>…</CardBody>
  <CardFooter>…</CardFooter>
</Card>
<div class="card">
  <div class="card-header">
    <h3 class="card-title">Sending health</h3>
    <p class="card-description">Deliverability signals.</p>
  </div>
  <div class="card-body">…</div>
  <div class="card-footer">…</div>
</div>
Input

Forms

Fields

Label/aria wiring is automatic in React. Inputs hold ≥16px text on mobile (no iOS zoom) and show the azure focus ring.

We'll never share it.
A name is required.
import { Input, Select, CodeInput } from '@readysetcloud/ui';

<Input label="Email address" type="email" hint="We'll never share it." />
<Input label="Newsletter name" error="A name is required." />
<Select label="Tier"><option>Free</option></Select>
<CodeInput label="Verification code" />
<div class="field">
  <label class="field-label" for="email">Email address</label>
  <input class="input" id="email" type="email">
  <span class="field-hint">We'll never share it.</span>
</div>

<input class="input input-error">
<span class="field-error" role="alert">A name is required.</span>
Feedback

Alerts, toasts & modals

Alert

Inline outcome messaging. Error alerts get role="alert".

Issue #204 scheduled for Monday 9:00 AM.
Your sender domain is pending DNS verification.
import { Alert } from '@readysetcloud/ui';

<Alert variant="success">Issue #204 scheduled.</Alert>
<Alert variant="error">We couldn't reach SendGrid.</Alert>
<div class="alert alert-success">Issue #204 scheduled.</div>
<div class="alert alert-error" role="alert">We couldn't reach SendGrid.</div>

Toast

Transient confirmations, bottom-right (full-width on phones). Mount ToastProvider once; errors linger 8s, others 5s.

Subscriber imported.
Import failed — bad CSV header.
import { ToastProvider, useToast } from '@readysetcloud/ui';

const { toast } = useToast();
toast('Subscriber imported.', { variant: 'success' });
<div class="toast-viewport">
  <div class="toast toast-success">Subscriber imported.</div>
</div>

Modal

Native <dialog> — Esc/backdrop close and focus trapping for free. Becomes a bottom sheet on small screens.

Delete issue #204?

This can't be undone. Subscribers who already received it are unaffected.

import { Modal } from '@readysetcloud/ui';

<Modal open={open} onClose={() => setOpen(false)} aria-label="Delete issue">
  …
</Modal>
<dialog class="modal" id="confirm" aria-label="Delete issue">…</dialog>
<script>document.getElementById('confirm').showModal();</script>
Waiting

Loading & empty states

Spinner, skeleton, progress

Use the composed loading states from the package instead of app-local loaders.

Sending…
Domain added
DNS propagating
Send test email
import { InlineLoading, Skeleton, ProgressIndicator } from '@readysetcloud/ui';

<InlineLoading isLoading loadingText="Sending…" />
<Skeleton width="75%" />
<ProgressIndicator steps={[
  { id: 'domain', label: 'Domain added', status: 'completed' },
  { id: 'dns', label: 'DNS propagating', status: 'in-progress' },
  { id: 'test', label: 'Send test email', status: 'pending' }
]} />
<span class="spinner"></span>
<div class="skeleton" style="width: 75%; height: 1rem;"></div>
<div class="progress-step" data-status="completed">…</div>

EmptyState

No issues yet Write your first issue and it will show up here.
import { EmptyState, Button } from '@readysetcloud/ui';

<EmptyState
  title="No issues yet"
  description="Write your first issue and it will show up here."
  icon="📭"
  action={<Button size="sm">New issue</Button>}
/>
<div class="empty-state">
  <span class="empty-state-title">No issues yet</span>
  <span>Write your first issue and it will show up here.</span>
</div>