Banner

Free · MITReact Native · Expo

An 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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/banner.json

Then 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

Use it for
  • 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).
Reach for something else
  • 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

PropTypeDefault
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`.
booleantrue
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`.
booleantrue
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.
number0
haptic
Warning and error haptics when a warning or danger banner appears after mount, tap on dismiss. Default `true`.
booleantrue
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

PropTypeDefault
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.
number2500
spacing
Space under the banner that opens and closes with it. Default 0.
number0
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.