Footer
A row of actions that stays put over the body, stands on the safe-area band, and lands on the keyboard's edge.
<BottomSheet.Container>
<BottomSheet.Background style={styles.background} />
<BottomSheet.Handle style={styles.handle}>
<View style={styles.pill} />
</BottomSheet.Handle>
<BottomSheet.Content style={styles.content}>
<BottomSheet.Title style={styles.title}>Discard changes?</BottomSheet.Title>
<BottomSheet.Description style={styles.description}>
Two lines of copy and a footer. The whole sheet is as tall as the four of them.
</BottomSheet.Description>
</BottomSheet.Content>
<BottomSheet.Footer padding={16} style={styles.footer}>
<Pressable onPress={() => choose("keep")} style={styles.action}>
<Text style={styles.actionLabel}>Keep</Text>
</Pressable>
<Pressable onPress={() => choose("discard")} style={styles.action}>
<Text style={styles.actionLabel}>Discard</Text>
</Pressable>
</BottomSheet.Footer>
</BottomSheet.Container>Footer is written beside the body — after Content, a scrollable or Steps, inside
Container — never inside it. One per sheet.
Sticky or inline
sticky (default) | sticky={false} | |
|---|---|---|
| Position | absolute over the body, at the sheet's bottom | a plain View in the panel's flow |
| Scrolls with a list | no — the list's last row stops above it | yes |
| On the keyboard | rides it by transform, in the same frame | moves with the sheet |
| Counts in the dynamic snap point | yes | yes |
The body is clamped to the area above a sticky footer, and a scrollable's marginBottom follows
the footer's height, so the last row and the scroll indicator stop above the buttons rather than
under them.
The band
An attached sheet's bottom edge is the screen's bottom edge, and the safe-area inset — the home
indicator — is between the two. The footer stands on that band: bottomInset of empty space
under the buttons, counted once in the sheet's height whether there is a footer or not. A sheet
with no footer reserves the same band under its content.
+------------------------------+ <-- the sheet's top
handle | ==== | ^
+------------------------------+ |
content | Discard changes? | |
| Two lines of copy. | | dynamicSnapPoint
+------------------------------+ |
footerContent | [ Keep ] [ Discard ] | |
+------------------------------+ |
band | ______ | v bottomInset, the home indicator
+------------------------------+ <-- the screen's bottom edgedynamicSnapPoint = handle + content + footerContent + bandPass bottomInset={insets.bottom} from useSafeAreaInsets(); the package reads no insets of its
own. A detached card floats above the inset instead, so its band is zero.
The band collapses with the keyboard
When the keyboard rises the sheet lifts by the keyboard's height less the band times
progress, so the band is what collapses: at full keyboard the footer's bottom edge is exactly
on the keyboard's top edge, with no home-indicator's height of dead space between them.
progress = 0 progress = 1
+----------------------+
| handle |
+----------------------+ | content |
| handle | +----------------------+
| content | | footerContent |
+----------------------+ +======================+ <-- the keyboard's top edge
| footerContent | | : : : : : : : : : : |
+----------------------+ | : : keyboard : : : : |
| band | | : : : : : : : : : : |
+----------------------+ +----------------------+ <-- the screen's bottom edge
height = base height = base + keyboard - bandfooterTop = height − keyboard − footerContent − band · (1 − progress)
= base − footerContent − band for every progressfooterTop is measured down from the sheet's top. On the left the footer ends a band above the
screen's edge; on the right the sheet is taller by the keyboard less that band, so the footer ends
on the keyboard and the band is the part the keys cover.
The footer's position inside the panel is therefore constant while the keyboard animates; only
the panel moves. That is the property the library this replaces committed React state per keyboard
frame to approximate, and it is proved by a test that sweeps progress from 0 to 1 in twenty
steps rather than by a screenshot.
Padding
The footer is one view. padding and style are flattened onto it together, so padding from
either counts in footerContent and therefore in the sheet's height. The footer then adds the band
to its own bottom padding, which is what carries the surface's colour down to the sheet's bottom
line; the band is taken back off the measurement, so it is counted once, as the band.
+----------------------------------------+ <-- footerTop, and the hairline from style
| padding | ^
| +------------+ +------------+ | | footerContent
| | Keep | | Discard | | | the measured height, padding included
| +------------+ +------------+ | |
| padding | v
| - - - - - - - - - - - - - - - - - - |
| band, added by the footer itself | bottomInset, not measured
+----------------------------------------+ <-- the sheet's bottom lineUse padding for the space around the buttons and style for the surface and the hairline. A
paddingBottom given as a string cannot be added to the band and is read as zero.
<BottomSheet.Footer padding={16} style={{ backgroundColor: "#2C2C2E", borderTopWidth: StyleSheet.hairlineWidth }}>Content's footerGap adds space between the last line of content and the footer's top, for a
body that ends in a field.
With steps
A footer written beside a Steps body reads the machine through
useSheetStep() and shows the step's actions. It is not a descendant of Steps, so Steps
registers its controller with the root for the footer to find.
API Reference
Prop
Type