Banner
Free · MITReact Native · ExpoAn inline status banner that slides down from under the edge above and pushes the content down, flings away sideways with the finger, and can collapse on its own behind a draining hairline; includes an offline preset.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/banner.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated, react-native-svg match your Expo SDK. Files land in components/goodui-native/banner.
Usage
import { useState } from "react";
import { Banner } from "@/components/goodui-native/banner/banner";
export function BillingNotice({ retryPayment }: { retryPayment: () => Promise<void> }) {
const [visible, setVisible] = useState(true);
return (
<Banner
visible={visible}
onVisibleChange={setVisible}
tone="danger"
title="Payment failed"
description="Your Visa ending 4242 was declined. Update it to keep Pro."
action={{ label: "Retry payment", onPress: retryPayment }}
spacing={16}
/>
);
}When to use
- A state of the screen or account the person should know about while they keep working: card expiring, sync paused, trial ending.
- A failure that belongs to one place on the screen, with a way out: 'Couldn't load new messages' with Retry.
- Connectivity: OfflineBanner at the top of a feed or a form.
- A short confirmation tied to the content below it that can collapse on its own (duration).
- Confirming an action the person just took somewhere else on screen: let the control show its own result (button, swipe-to-confirm), or use toast for background events.
- A decision that blocks progress: use alert-dialog.
- A first-run walkthrough: use coach-mark.
- A whole screen with nothing to show: use empty-state.
Examples
Offline preset wired to your reachability state
<OfflineBanner offline={!isConnected} onRetry={() => refetch()} spacing={12} />Collapses on its own after 4 seconds
<Banner tone="success" title="Booking moved to Friday, 10:30" duration={4000} visible={moved} onVisibleChange={setMoved} />Solid, not dismissible
<Banner variant="solid" tone="warning" dismissible={false} title="Maintenance tonight from 01:00 to 02:00" />API reference
Banner
| Prop | Type | Default |
|---|---|---|
| title* One line that says what happened. Sentence case. | string | — |
| description Optional second line with the detail or the way out. | string | — |
| tone `info` uses the accent; the others are status colors and only mean status. Changing it crossfades the surface. | "success" | "warning" | "danger" | "info" | "info" |
| variant `soft` is a tinted fill, `solid` a full status color for the one thing that matters, `outline` a hairline on the surface. | "solid" | "soft" | "outline" | "soft" |
| visible Controlled visibility. Pair with `onVisibleChange`. | boolean | — |
| defaultVisible Uncontrolled starting visibility. Default `true`. | boolean | true |
| onVisibleChange Called with `false` when the banner closes itself (× button, swipe, action, timer). | (visible: boolean) => void | — |
| action One action under the text. | BannerAction | — |
| dismissible Shows an × button. Default `true`. | boolean | true |
| swipeToDismiss Swipe the banner sideways to fling it away. Default: on when `dismissible`. | boolean | — |
| duration Milliseconds before it collapses on its own. A hairline drains as the timer runs; touch or hover pauses it. Omit to keep it. | number | — |
| icon Replaces the tone icon. A function receives the icon color. `false` hides it. | ReactNode | ((color: string) => ReactNode) | false | — |
| spacing Space under the banner that opens and closes with it, so the content below never jumps. Default 0. | number | 0 |
| haptic Warning and error haptics when a warning or danger banner appears after mount, tap on dismiss. Default `true`. | boolean | true |
| announcement Announced to screen readers when the banner appears after mount. Defaults to the title and description. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<BannerLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
OfflineBanner
| Prop | Type | Default |
|---|---|---|
| offline* Whether the device is offline right now. Wire it to NetInfo or your own reachability check. | boolean | — |
| onRetry Shows a Retry action while offline. Return a promise to show a spinner on it. | () => unknown | — |
| labels Overrides for the built-in copy; a GooduiLocaleProvider pack sets it app-wide. | Partial<OfflineBannerLabels> | — |
| onlineDuration Milliseconds the "Back online" state stays before collapsing. Default 2500. | number | 2500 |
| spacing Space under the banner that opens and closes with it. Default 0. | number | 0 |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Show: the outer height opens to the measured content on the smooth spring while the card slides down from under the edge above (translateY from minus its height), so the content below is pushed, never jumps. Hide runs the same in reverse.
- Swipe: the card follows the finger 1:1 sideways and fades with distance; past 35 % of its width or a fast flick it springs off with the release velocity and the height closes behind it, otherwise it snaps back on the snappy spring.
- A tone or variant change crossfades the surface, text and icon disc colors on the UI thread (OfflineBanner uses this for offline to back online). The success check draws itself in each time the banner appears.
- duration: a 2 pt hairline drains across the bottom; a touch (or hover on the web) pauses it and it resumes where it stopped.
- A rejected action shakes the card.
- Reduced motion: the height changes at once and the card fades, no slide; the fling is a short timing; no shake; the check appears without drawing. The timer still runs.
- Haptic: warning or error when a warning or danger banner appears after mount (not on first render)
- Haptic: tap on dismiss and on the action; success or error when an async action settles
- Haptic: haptic={false} turns all of them off for one banner; OfflineBanner plays none
- Height, slide, swipe, colors and the timer run on the UI thread. The content is measured once per real size change.
- A hidden banner stays mounted at zero height; unmount it yourself if it will never come back.
Accessibility
- When a banner appears after mount its title and description (or `announcement`) are announced once; it is not a live region, so nothing is read twice.
- On iOS and Android warning and danger banners carry the alert role, info and success the summary role.
- The × is an IconButton labelled 'Dismiss'; the action is a button with busy and disabled in accessibilityState and as aria-busy and aria-disabled on the web.
- The action label sits on a plain surface pill (on the solid variant, a pill in the tone's on color), never on a tint of its own status color, so its 13 pt label keeps 4.5:1 in light and dark for every tone and accent.
- A failed async action announces '<label> failed. Try again.'.
- While collapsed the banner is hidden from screen readers (accessibilityElementsHidden, no-hide-descendants, aria-hidden) and ignores touches.
- Swipe to dismiss is a shortcut only: the × button does the same.
Notes for AI
- Do not put the banner in a parent with `gap`: the gap stays when it collapses. Use `spacing` for the space under it, which opens and closes with it.
- Controlled: pass visible and onVisibleChange; the banner calls onVisibleChange(false) on ×, swipe, a dismissing action and when duration runs out.
- action.onPress may return a promise: spinner while pending, the banner closes on resolve (unless action.dismiss is false) and shakes on reject. Do not add your own loading state.
- tone='info' uses the accent. Success, warning and danger only for status.
- OfflineBanner takes a boolean; wire it to @react-native-community/netinfo or your own check. It shows 'Back online' for onlineDuration ms when the boolean flips back.
Related
Background confirmations that stack at the top, fan out on tap, swipe away up or sideways, and pause their timers while touched.
A centered alert that springs in from slightly larger; touch an action and slide across the others with a tick per action, tap outside and it nudges, and async actions keep it open with a spinner until they settle.
A designed empty, error or offline state: a tinted disc pops in, its line art draws itself stroke by stroke with the highlight landing last, then the title, description and actions rise in.
A layered icon-only button whose surface and icon sink on touch down and release on different springs; it runs async actions itself, carries a badge and shows a hint on long press.