Skip to content

Components

Cloud bar

Cloud cover as a label, a percentage and an eight pixel track whose glowing fill animates to the new value over a second.

Preview

Cloud Cover45%

Variants

Pick a variant with the controls. The preview and both code blocks follow.

State
45%
Cloud Cover45%
Variant.tsx
<AppCloudBar value={45} />

Implementation

The React tab is this site's own component, the same source that draws the preview, styled by the CSS below it. The Flutter tab is the Dart a screen builds it with, from the app's tokens.

Usage
<AppCloudBar value={45} /><AppCloudBar value={0} state="loading" />
app-widgets.tsx · AppCloudBar
/** CloudBar from cloud_bar.dart: label, percentage, and an 8 dp track with the glowing fill. */export function AppCloudBar({  value,  state = "data",  message,}: {  value: number;  state?: "data" | "loading" | "error";  message?: string;}) {  return (    <div className="app-ui app-cloudbar">      <div className="app-cloudbar-head">        <span className="app-label-md">Cloud Cover</span>        {state === "loading" ? (          <span className="app-spinner" role="status" aria-label="Loading" />        ) : state === "error" ? (          <span className="app-heading" aria-label="Error" style={{ color: "var(--c-error)" }}>!</span>        ) : (          <span className="app-label-md">{Math.round(value)}%</span>        )}      </div>      {state === "error" ? (        <p className="app-cloudbar-error">{message ?? "Weather unavailable"}</p>      ) : (        <div className="app-cloudbar-track">          <div className="app-cloudbar-fill" style={{ width: `${state === "loading" ? 0 : Math.min(100, Math.max(0, value))}%` }} />        </div>      )}    </div>  );}
app-ui.css
.app-cloudbar {  box-sizing: border-box;  width: 100%;  padding: var(--spacing-f13) var(--spacing-f21) var(--spacing-f21);  border-radius: var(--radius-f13);  border: var(--spacing-f1) solid var(--hair);  background: var(--sheen), var(--fill);  box-shadow: var(--highlight);}.app-cloudbar-head { display: flex; justify-content: space-between; align-items: center; }.app-cloudbar-track { margin-top: var(--spacing-f8); height: var(--spacing-f8); border-radius: var(--radius-full); background: var(--track); }.app-cloudbar-fill { height: 100%; border-radius: var(--radius-full); background: var(--c-accent); box-shadow: var(--glow); transition: width 1s cubic-bezier(0.215, 0.61, 0.355, 1); }.app-spinner { width: var(--spacing-f13); height: var(--spacing-f13); box-sizing: border-box; border-radius: var(--radius-full); border: var(--spacing-f2) solid var(--c-text); border-right-color: transparent; animation: app-spin 1s linear infinite; }

API reference

React propFlutter parameterTypeDefaultMeaning
valuecloudCoverPercentagenumber / doublerequired0 to 100.
stateisLoading, errorMessagedata, loading, errordataWhich of the three bodies to show.
messageerrorMessagestringnoneShown in the error state.

Design tokens

Every value on this component is a system token. The last column is what the app widget uses today, so the gap to close is visible.

RoleCSS tokenDart tokenApp widget today
Surface--sheen (space-grey-50 at --mag-5 to --mag-8) over space-grey-950 at --mag-1AstrColors.spaceGrey with Mag.m5, m8, m1AppColors.surfaceGlass #CC0C0F11
Borderspace-grey-50 at --mag-6Mag.m6borderSubtle #0C1E33
Radius--radius-f13AstrRadius.f13kInnerRadius 12
Padding--spacing-f13 --spacing-f21AstrSpace.f13, f2118 and 20
Trackspace-grey-500 at --mag-4, height --spacing-f8AstrColors.spaceGrey[500], Mag.m4, AstrSpace.f8cloudTrack #4D3C4957, 8
Fill--color-deep-space-200 with glowAstrColors.deepSpace[200]accentSecondary #3A96FF
Label--text-s-1AstrType.sNeg1labelMd 12