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
Variants
Pick a variant with the controls. The preview and both code blocks follow.
State
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; }Usage
const CloudBar(cloudCoverPercentage: 45)const CloudBar(cloudCoverPercentage: 0, isLoading: true)astr_glass.dart · AstrGlassCard
/// The glass surface for cards: a sheen over the surface and a hairline edge./// Cards do not glow; glow belongs to buttons and tiles. Mini cards are/// [AstrSpace.f144] wide and 144 + 21 tall.class AstrGlassCard extends StatelessWidget { const AstrGlassCard({ super.key, required this.child, this.width, this.height, }); final Widget child; final double? width; final double? height; @override Widget build(BuildContext context) { final Color text = AstrColors.spaceGrey[50]!; final BorderRadius radius = BorderRadius.circular(AstrRadius.f21); return RepaintBoundary( child: ClipRRect( borderRadius: radius, child: BackdropFilter( filter: ImageFilter.blur(sigmaX: AstrSpace.f13, sigmaY: AstrSpace.f13), child: Container( width: width, height: height, padding: const EdgeInsets.all(AstrSpace.f13), decoration: BoxDecoration( borderRadius: radius, border: Border.all(color: text.withValues(alpha: Mag.m5)), color: _ink.withValues(alpha: Mag.m1), gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: <Color>[ text.withValues(alpha: Mag.m5), text.withValues(alpha: Mag.m8), ], ), ), child: child, ), ), ), ); }}cloud_bar.dart · CloudBar
class CloudBar extends StatefulWidget { const CloudBar({ super.key, required this.cloudCoverPercentage, this.isLoading = false, this.errorMessage, }); final double cloudCoverPercentage; // 0-100 final bool isLoading; final String? errorMessage; @override State<CloudBar> createState() => _CloudBarState();}cloud_bar.dart · _CloudBarState
class _CloudBarState extends State<CloudBar> { @override Widget build(BuildContext context) { return Container( width: double.infinity, height: 84, decoration: BoxDecoration( color: AppColors.surfaceGlass, border: Border.all(color: AppColors.borderSubtle), borderRadius: BorderRadius.circular(AppSpacing.kInnerRadius), ), padding: const EdgeInsets.fromLTRB(18, 20, 18, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Text( 'Cloud Cover', style: AppTypography.labelMd.copyWith(color: AppColors.textPrimary), ), if (widget.isLoading) const SizedBox( width: 12, height: 12, child: CircularProgressIndicator(strokeWidth: 2, color: AppColors.textPrimary), ) else if (widget.errorMessage != null) const Icon(Icons.error_outline, color: Colors.redAccent, size: 16) else Text( '${widget.cloudCoverPercentage.toInt()}%', style: AppTypography.labelMd.copyWith(color: AppColors.textPrimary), ), ], ), const SizedBox(height: 13), if (widget.errorMessage != null) Text( widget.errorMessage!, style: AppTypography.labelSm.copyWith(color: Colors.redAccent), ) else Container( height: AppSpacing.kCloudTrackHeight, width: double.infinity, decoration: BoxDecoration( color: AppColors.cloudTrack, borderRadius: BorderRadius.circular(AppSpacing.kInnerRadius), ), child: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Stack( children: <Widget>[ AnimatedContainer( duration: const Duration(milliseconds: 1000), curve: Curves.easeOutCubic, width: constraints.maxWidth * (widget.cloudCoverPercentage / 100), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: AppColors.accentSecondary, boxShadow: AppColors.glowShadow, ), ), ], ); }, ), ), ], ), ); }}API reference
| React prop | Flutter parameter | Type | Default | Meaning |
|---|---|---|---|---|
| value | cloudCoverPercentage | number / double | required | 0 to 100. |
| state | isLoading, errorMessage | data, loading, error | data | Which of the three bodies to show. |
| message | errorMessage | string | none | Shown 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.
| Role | CSS token | Dart token | App widget today |
|---|---|---|---|
| Surface | --sheen (space-grey-50 at --mag-5 to --mag-8) over space-grey-950 at --mag-1 | AstrColors.spaceGrey with Mag.m5, m8, m1 | AppColors.surfaceGlass #CC0C0F11 |
| Border | space-grey-50 at --mag-6 | Mag.m6 | borderSubtle #0C1E33 |
| Radius | --radius-f13 | AstrRadius.f13 | kInnerRadius 12 |
| Padding | --spacing-f13 --spacing-f21 | AstrSpace.f13, f21 | 18 and 20 |
| Track | space-grey-500 at --mag-4, height --spacing-f8 | AstrColors.spaceGrey[500], Mag.m4, AstrSpace.f8 | cloudTrack #4D3C4957, 8 |
| Fill | --color-deep-space-200 with glow | AstrColors.deepSpace[200] | accentSecondary #3A96FF |
| Label | --text-s-1 | AstrType.sNeg1 | labelMd 12 |