GlassKit UI
Primitives

AsyncView

The four-state async renderer every data screen needs: placeholder → loading → success / error. You own the async work and pass the status; AsyncView picks the view, with lens-ready defaults.

Heart rate128BPM
Drive the async state machine

Installation

npx @glasskit-ui/cli add async-view

Install the SDK (it provides GlassViewport, useDpad and the stylesheet), then copy these files into your project:

npm install @glasskit-ui/react
// components/lib/utils.tsimport { clsx, type ClassValue } from "clsx";import { twMerge } from "tailwind-merge";export type { ClassValue };/** * Merge class names the shadcn way: clsx joins conditionals, tailwind-merge * de-dupes conflicting Tailwind utilities so a consumer's `className` override * wins (e.g. passing `px-2` beats the component's `px-6`). Lens components are * Tailwind utilities + `--gk-*` tokens, so this de-dupe matters. */export function cn(...inputs: ClassValue[]): string {  return twMerge(clsx(inputs));}/** * Accessible name from a free-form `label` prop: the label itself when it's a * plain string, otherwise undefined (a ReactNode can't become an aria-label). */export function stringLabel(label: unknown): string | undefined {  return typeof label === "string" ? label : undefined;}
// components/glasskit/async-view.tsximport type { ReactNode } from "react";import { cn } from "../lib/utils";export type AsyncStatus = "idle" | "loading" | "success" | "error";/** * <AsyncView> — the four-state async renderer every data screen needs: * placeholder (idle) → loading → success/error. The consumer owns the * async work and passes the current `status`; AsyncView picks the view. * This is the spine's one styled-with-logic piece — the logic is the * state→view selection, nothing more. * * Sensible additive defaults: an emitted pulse for loading, a dim line * for error. Override any state via its slot. */export function AsyncView({  status,  children,  loading,  error,  placeholder,  errorLabel = "Couldn’t load",  className,}: {  status: AsyncStatus;  /** Success content. */  children?: ReactNode;  loading?: ReactNode;  error?: ReactNode;  placeholder?: ReactNode;  /** Default error message when no `error` slot is given. */  errorLabel?: ReactNode;  className?: string;}) {  if (status === "success") return <>{children}</>;  let body: ReactNode;  if (status === "loading") {    body = loading ?? <Spinner />;  } else if (status === "error") {    body = error ?? (      <p className="t-body text-muted-foreground">{errorLabel}</p>    );  } else {    body = placeholder ?? null;  }  return (    <div      className={cn(        "flex flex-col items-center justify-center gap-3 text-center",        className,      )}      role="status"      aria-busy={status === "loading"}    >      {body}    </div>  );}/** The default loading indicator — three emitted dots pulsing in sequence. */export function Spinner({ label = "Loading" }: { label?: string }) {  return (    <span className="gk-spinner" role="img" aria-label={label}>      <span />      <span />      <span />    </span>  );}

Usage

<AsyncView status={status} error={<span className="t-caption text-foreground-faint">Couldn’t load</span>}>  <Readout label="Heart rate" value={bpm} unit="BPM" /></AsyncView>

Props

PropTypeDefaultDescription
status"idle" | "loading" | "success" | "error"Which view to render.
childrenReactNodeSuccess content.
loadingReactNodeOverride the default Spinner.
errorReactNodeOverride the default error line.
placeholderReactNodeShown when idle.
errorLabelReactNode"Couldn’t load"Default error message when no error slot is given.
classNamestringExtra classes for the centered state wrapper (idle / loading / error).

Loading, empty, and error

AsyncView is the spine for data screens. Give it status and the three slots:

<AsyncView
  status={status}
  placeholder={
    <EmptyState title="No workouts" hint="Start one on your phone." />
  }
  error={<ErrorState title="No signal" onRetry={refetch} />}
>
  {data}
</AsyncView>
  • EmptyState is for when nothing failed and there is just no content yet. It reads quiet, with an optional invite action.
  • ErrorState is for when something failed and the wearer can retry. No red: the lens has one accent, so the words carry the failure.
  • The default loading slot is the pulsing Spinner; override it for skeleton-style screens.

On this page