About this animation
An offline feed with retry feedback and a reconnection reveal, with configurable articles, status labels, accent color, and fonts.
Why this one
Most connection notices stop at a generic error. This one explains what is still available: saved stories remain readable while unsaved cards fade to 45%. Three Lucide Wi-Fi arcs light up in sequence during retry. Recovery turns the actual banner green, draws a check, folds the surface into a departing pill, restores the cards in a top-to-bottom cascade, and inserts a fresh story that pushes the list down.
How it works
- A ref exposes
goOffline(),retry(),restore(), andreset(); React holds only discrete status and container layout. - Shared values drive banner presence, contraction, connectivity arcs, countdown, and story insertion on the UI thread. Each card derives its dimming target from the connection progress and delays both directions by 50 ms per position.
onRetryresolvestruefor recovery orfalsefor failure. Rejections are failures too. A generation token discards stale requests when a transition interrupts them.- Failure shakes the material and starts a real five-second countdown. A retry can interrupt it immediately; reaching zero retries automatically. Requests and timers are cleaned up on unmount.
- The countdown holds at each integer and rolls only in the final 180 ms of a second. Reduced motion uses static figures and 200 ms fades instead of flights, shakes, rotation, or scaling.
- Article images are supplied by the host, and the scrollable feed measures its own container with
onLayout.
Install
npx expo install @shopify/react-native-skia@2.14.0 expo-image@~58.0.17 react-native-gesture-handler@~3.3.0 react-native-reanimated@4.7.1 react-native-worklets@0.13.0Copy OfflineRetryBanner.tsx, icons.ts, and index.ts together. Wrap the app in GestureHandlerRootView. For web, load CanvasKit before mounting this component (Expo/Skia's WithSkiaWeb or the host's existing lazy Skia loader). The component uses Skia and must not be statically mounted before that loader is ready. Fonts are optional host props, with system fonts as the fallback.
Icons: Lucide (ISC). Paths are copied from the project's kit. The Lumen tile and editorial photography belong only to the demo, which additionally uses expo-linear-gradient.
Usage
import { useRef } from 'react';
import { OfflineRetryBanner, type OfflineRetryBannerRef } from './offline-retry-banner';
const connection = useRef<OfflineRetryBannerRef>(null);
<OfflineRetryBanner
ref={connection}
articles={articles}
onRetry={async () => {
try {
return (await fetch('https://your-service.example/health')).ok;
} catch {
return false;
}
}}
/>;
// Call from the application's network subscription:
connection.current?.goOffline();A personalized variant:
<OfflineRetryBanner
ref={connection}
articles={articles}
newArticle={latestStory}
accentColor="#0EA5E9"
successColor="#16A34A"
retrySeconds={5}
colorScheme="dark"
fontFamily="YourInterfaceFont"
titleFontFamily="YourEditorialFont"
monoFontFamily="YourNumberFont"
onRetry={checkConnection}
onArticlePress={openStory}
onStatusChange={handleConnectionStatus}
/>The host owns connectivity, actual caching, article retrieval, and navigation. This component displays availability and blocks onArticlePress for unsaved stories while offline; it does not download or persist articles.
Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
articles | readonly OfflineArticle[] | [] | Articles to display. |
newArticle | OfflineArticle | undefined | Optional story inserted after recovery. |
onRetry | () => Promise<boolean> | undefined | Resolve true for recovery, false for failure; rejection also means failure. |
onStatusChange | ( status: ConnectionStatus, ) => void | undefined | Discrete connection state callback. |
onArticlePress | ( article: OfflineArticle, ) => void | undefined | Called when a readable article is pressed. |
onInteraction | () => void | undefined | Called for touch or gesture input, allowing the host to stop autoplay. |
fontFamily | string | undefined | Interface font; defaults to the system font. |
titleFontFamily | string | undefined | Editorial headline font; defaults to the system font. |
monoFontFamily | string | undefined | Countdown font; defaults to the system font. |
accentColor | string | '#0EA5E9' | Accent used for saved stories and fresh content. |
successColor | string | '#16A34A' | Recovery color. |
retrySeconds | number | 5 | Automatic retry delay in seconds, clamped to 1–9. |
colorScheme | 'light' | 'dark' | 'auto' | 'auto' | Light, dark, or device palette. |
offlineLabel | string | "You're offline — showing saved articles" | Offline explanation. |
restoredLabel | string | 'Back online' | Recovery announcement. |
retryLabel | string | 'Retry' | Retry button and accessibility label. |
newStoriesLabel | string | '2 new stories' | Label on the inserted story. |
style | StyleProp<ViewStyle> | undefined | Container style. |
testID | string | undefined | Root test identifier. |
duration | number | 760 | Retry spinner cycle in milliseconds; request timing is unchanged. Spring physics and reduced-motion timing are unchanged. |
Demo timeline
Online at 0 ms → offline at 1000 → first retry at 1570 → failure at 1790 → manual second retry at 2350 → recovery at 2570. The check draws and the fresh story pushes into the feed before 3000 ms. Contraction starts 580 ms after recovery; the status returns to online 1080 ms after recovery. The full cycle repeats every 7600 ms so the recovered feed has a readable rest. Any touch stops playback. The second demo retry is deliberately manual: the complete five-second automatic countdown remains available during manual use.
The demo includes Light/Dark controls, a connection simulation button, and a local reading panel. All three photographs are bundled. No network is needed to demonstrate either saved reading or connection recovery.
Compatibility
Expo SDK 58; React Native 0.88.0; Reanimated 4.7.1; worklets 0.13.0; Skia 2.14.0. Intended for iOS, Android, and web. Compatible with the project's SDK 58 Expo Go runtime; custom native configurations require a development build.
Accessibility
Retry and article controls have button roles and labels. A swipe on the status icon retries; the Retry button provides the equivalent tap action. Important connection transitions announce through AccessibilityInfo and a polite live region. Countdown digits are hidden inside a single accessible group to avoid announcing each rolling glyph. Saved cards remain readable offline. The demo theme control has switch semantics. Touch targets are at least 44 pt. Reduced motion removes translations, shaking, sway, rolling digits, repeated waves, and press scaling, using 200 ms fades for state changes.
Implementation notes
Layout: the header reserves the top-right viewer-close area. Headline/image proportions reserve enough room for two title lines and metadata. At 390×844 and 320×694, content remains in a scrollable feed rather than compressing all cards to fit; smaller screens require scrolling to the final story. The recovery begins at 2.57 seconds. Effects are actual surface color, path opacity/drawing, material shadow, and layout changes; no block sweeps over text.




