Modal
Modals are versatile dialogs that can display critical information, require user interaction, or show supplementary content without navigating away from the current screen.
Installation
$ cbui-cli install modal
cbui-cli
globally and authenticated your account. This will ensure you can access all the necessary features.Import
1import {
2 Modal,
3 ModalContent,
4 ModalHeader,
5 ModalBody,
6 ModalFooter,
7 ModalContext,
8 useModalContext
9} from '@/crossbuildui/modal';
10import { Button } from '@/crossbuildui/button';
11import { Text, View } from 'react-native';
12import React, { useState } from 'react';
Basic Usage
The Modal component is controlled via the isOpen
and onOpenChange
props. It typically wraps ModalContent
, which in turn contains ModalHeader
, ModalBody
, and ModalFooter
.
1function App() {
2 const [isOpen, setIsOpen] = useState(false);
3
4 return (
5 <View>
6 <Button onPress={() => setIsOpen(true)}>Open Modal</Button>
7 <Modal
8 isOpen={isOpen}
9 onOpenChange={setIsOpen}
10 >
11 <ModalContent>
12 <ModalHeader>Modal Title</ModalHeader>
13 <ModalBody>
14 <Text>This is the body of the modal.</Text>
15 <Text>You can put any content here.</Text>
16 </ModalBody>
17 <ModalFooter>
18 <Button variant="ghost" onPress={() => setIsOpen(false)}>Cancel</Button>
19 <Button onPress={() => setIsOpen(false)}>Confirm</Button>
20 </ModalFooter>
21 </ModalContent>
22 </Modal>
23 </View>
24 );
25}
Composition
The Modal system is designed to be composable, allowing you to structure your modal content flexibly:
Modal
: The main wrapper that controls visibility, backdrop, and placement.ModalContent
: The container for the modal's dialog. Styles like size, radius, and shadow are applied here.ModalHeader
: Typically contains the title and an optional close button.ModalBody
: The main content area of the modal. Can be scrollable.ModalFooter
: Usually contains action buttons like "Confirm" or "Cancel".
size
, radius
, shadow
, scrollBehavior
, glass props (isGlass
, glassTint
, glassIntensity
), animationProps
, and close handlers are passed down from Modal
to its children via React Context, simplifying their usage.Sizes
Control the modal's width (and height for full
) using the size
prop. Available sizes: xs
, sm
, md
(default), lg
, xl
, 2xl
, 3xl
, 4xl
, 5xl
, full
.
1import type { ModalSize } from '@crossbuildui/modal';
2function App() {
3 const [size, setSize] = useState<ModalSize>('md');
4 const [isOpen, setIsOpen] = useState(false);
5
6 const openModal = (s) => { setSize(s); setIsOpen(true); };
7
8 return (
9 <View style={{ flexDirection: 'row', gap: 8 }}>
10 <Button onPress={() => openModal('sm')}>Open SM</Button>
11 <Button onPress={() => openModal('lg')}>Open LG</Button>
12 <Button onPress={() => openModal('full')}>Open Full</Button>
13
14 <Modal isOpen={isOpen} onOpenChange={setIsOpen} size={size}>
15 <ModalContent>
16 <ModalHeader>Modal Size: {size.toUpperCase()}</ModalHeader>
17 <ModalBody><Text>Content for {size} modal.</Text></ModalBody>
18 <ModalFooter><Button onPress={() => setIsOpen(false)}>Close</Button></ModalFooter>
19 </ModalContent>
20 </Modal>
21 </View>
22 );
23}
Backdrop
Customize the modal backdrop with the backdrop
prop:
opaque
(default): A semi-transparent dark overlay.blur
: Applies a blur effect to the content behind the modal (requiresexpo-blur
).transparent
: No visible backdrop.
1import type { ModalBackdrop } from '@crossbuildui/modal'; // Import the necessary types
2function App() {
3 const [backdrop, setBackdrop] = useState<ModalBackdrop>('opaque');
4 const [isOpen, setIsOpen] = useState(false);
5
6 const openModal = (b) => { setBackdrop(b); setIsOpen(true); };
7
8 return (
9 <View style={{ flexDirection: 'row', gap: 8 }}>
10 <Button onPress={() => openModal('opaque')}>Opaque</Button>
11 <Button onPress={() => openModal('blur')}>Blur</Button>
12 <Button onPress={() => openModal('transparent')}>Transparent</Button>
13
14 <Modal isOpen={isOpen} onOpenChange={setIsOpen} backdrop={backdrop}>
15 <ModalContent>
16 <ModalHeader>Backdrop: {backdrop}</ModalHeader>
17 <ModalBody><Text>Modal with {backdrop} backdrop.</Text></ModalBody>
18 <ModalFooter><Button onPress={() => setIsOpen(false)}>Close</Button></ModalFooter>
19 </ModalContent>
20 </Modal>
21 </View>
22 );
23}
Scroll Behavior
The scrollBehavior
prop determines how overflow content is handled:
outside
(default): TheModalContent
grows with its content. If it exceeds screen height, the entire backdrop area becomes scrollable.inside
: TheModalBody
becomes scrollable, while the header and footer remain fixed.ModalContent
itself will not exceed screen height.
1function App() {
2 const [isOpenInside, setIsOpenInside] = useState(false);
3 const [isOpenOutside, setIsOpenOutside] = useState(false);
4
5 const longContent = Array.from({ length: 30 }, (_, i) => `Item ${i + 1}`).join('\n');
6
7 return (
8 <View style={{ flexDirection: 'row', gap: 8 }}>
9 <Button onPress={() => setIsOpenInside(true)}>Scroll Inside</Button>
10 <Button onPress={() => setIsOpenOutside(true)}>Scroll Outside</Button>
11
12 <Modal isOpen={isOpenInside} onOpenChange={setIsOpenInside} scrollBehavior="inside">
13 <ModalContent style={{ height: 300 }}>
14 <ModalHeader>Scroll Inside</ModalHeader>
15 <ModalBody><Text>{longContent}</Text></ModalBody>
16 <ModalFooter><Button onPress={() => setIsOpenInside(false)}>Close</Button></ModalFooter>
17 </ModalContent>
18 </Modal>
19
20 <Modal isOpen={isOpenOutside} onOpenChange={setIsOpenOutside} scrollBehavior="outside">
21 <ModalContent>
22 <ModalHeader>Scroll Outside</ModalHeader>
23 <ModalBody><Text>{longContent}</Text></ModalBody>
24 <ModalFooter><Button onPress={() => setIsOpenOutside(false)}>Close</Button></ModalFooter>
25 </ModalContent>
26 </Modal>
27 </View>
28 );
29}
Placement
Position the modal on the screen using the placement
prop. Options include auto
(default, center for non-full, top for full), top
, center
, bottom
, top-center
, bottom-center
.
1import type { ModalPlacement } from '@crossbuildui/modal'; // Import the necessary types
2function App() {
3 const [placement, setPlacement] = useState<ModalPlacement>('center');
4 const [isOpen, setIsOpen] = useState(false);
5
6 const openModal = (p) => { setPlacement(p); setIsOpen(true); };
7
8 return (
9 <View style={{ flexDirection: 'row', gap: 8 }}>
10 <Button onPress={() => openModal('top')}>Top</Button>
11 <Button onPress={() => openModal('center')}>Center</Button>
12 <Button onPress={() => openModal('bottom')}>Bottom</Button>
13
14 <Modal isOpen={isOpen} onOpenChange={setIsOpen} placement={placement}>
15 <ModalContent>
16 <ModalHeader>Placement: {placement}</ModalHeader>
17 <ModalBody><Text>Modal placed at {placement}.</Text></ModalBody>
18 <ModalFooter><Button onPress={() => setIsOpen(false)}>Close</Button></ModalFooter>
19 </ModalContent>
20 </Modal>
21 </View>
22 );
23}
Dismissable Behavior
By default (isDismissable=
), modals can be closed by clicking the backdrop or pressing the hardware back button (Android) / Escape key (Web). Set isDismissable=
to prevent this.
1function App() {
2 const [isOpen, setIsOpen] = useState(false);
3
4 return (
5 <View>
6 <Button onPress={() => setIsOpen(true)}>Open Non-Dismissable Modal</Button>
7 <Modal
8 isOpen={isOpen}
9 onOpenChange={setIsOpen}
10 isDismissable={false}
11 >
12 <ModalContent>
13 <ModalHeader>Non-Dismissable Modal</ModalHeader>
14 <ModalBody>
15 <Text>This modal cannot be dismissed by clicking the backdrop or pressing Escape/Back.</Text>
16 </ModalBody>
17 <ModalFooter>
18 <Button onPress={() => setIsOpen(false)}>Close Manually</Button>
19 </ModalFooter>
20 </ModalContent>
21 </Modal>
22 </View>
23 );
24}
Glass Content Panel
Apply a frosted glass effect to the ModalContent
panel itself using the isGlass
prop on the main Modal
component. Customize its appearance with glassIntensity
(0-100) and glassTint
('light'
, 'dark'
, or 'default'
). When isGlass
is true, the ModalContent
's direct background color becomes transparent to allow the blur effect. Ensure content within the modal has sufficient contrast. This is different from backdrop="blur"
, which blurs the area behind the entire modal.
1function App() {
2 const [isOpen, setIsOpen] = useState(false);
3
4 return (
5 <View>
6 <Button onPress={() => setIsOpen(true)}>Open Glass Content Modal</Button>
7 <Modal
8 isOpen={isOpen}
9 onOpenChange={setIsOpen}
10 isGlass
11 glassIntensity={70}
12 glassTint="light" // or 'dark', 'default'
13 // backdrop="blur" // Can be combined with glass content
14 >
15 <ModalContent>
16 <ModalHeader>
17 <Text style={{ color: 'black' /* Adjust for contrast */ }}>Glass Content Panel</Text>
18 </ModalHeader>
19 <ModalBody>
20 <Text style={{ color: 'black' /* Adjust for contrast */ }}>This modal's content panel has a glass effect.</Text>
21 </ModalBody>
22 <ModalFooter><Button onPress={() => setIsOpen(false)}>Close</Button></ModalFooter>
23 </ModalContent>
24 </Modal>
25 </View>
26 );
27}
Animations
Apply animations to the ModalContent
using the animationProps
prop on the Modal
component. This prop accepts an object that will be spread onto an Animated.View
from react-native-reanimated
.
animationProps
requires react-native-reanimated
to be installed and configured in your project. Example: { entering: FadeIn, exiting: FadeOut }
1// Ensure you have 'react-native-reanimated' installed
2// import { FadeIn, FadeOut } from 'react-native-reanimated';
3
4function App() {
5 const [isOpen, setIsOpen] = useState(false);
6
7 return (
8 <View>
9 <Button onPress={() => setIsOpen(true)}>Open Animated Modal</Button>
10 <Modal
11 isOpen={isOpen}
12 onOpenChange={setIsOpen}
13 animationProps={{ entering: FadeIn.duration(500), exiting: FadeOut.duration(300) }}
14 >
15 <ModalContent>
16 <ModalHeader>Animated Modal</ModalHeader>
17 <ModalBody><Text>This modal uses animations.</Text></ModalBody>
18 </ModalContent>
19 </Modal>
20 </View>
21 );
22}
Using Modal Context
For advanced scenarios where child components deep within ModalContent
need to access properties of the Modal
or its state, you can use the useModalContext
hook. This hook provides access to ModalContext
, which includes properties like isOpen
, size
, placement
, scrollBehavior
, isGlass
, onClose
(the function to close the modal), and more.
Any component calling useModalContext
must be a descendant of a Modal
component (typically within ModalContent
).
onClose
from deep children might make state flow harder to track. Prefer using standard close mechanisms (buttons in footer, dismissable behavior) where possible.1import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useModalContext } from '@/crossbuildui/modal';
2import { Button } from '@/crossbuildui/button';
3import { Text, View } from 'react-native';
4import React, { useState } from 'react';
5
6const ModalSizeIndicator = () => {
7 const context = useModalContext();
8
9 if (!context) {
10 return <Text>Not in a Modal context</Text>;
11 }
12 // Note: onClose is available on context, but typically handled by ModalFooter buttons or isDismissable.
13 const { size, placement, scrollBehavior, isGlass } = context;
14
15 return (
16 <View style={{ padding: 8, backgroundColor: '#eef', borderRadius: 4, marginBottom: 8 }}>
17 <Text>Context: Size: {size}, Placement: {placement}</Text>
18 <Text>Scroll: {scrollBehavior}, Glass Content: {isGlass ? 'Yes' : 'No'}</Text>
19 </View>
20 );
21};
22
23// <Modal isOpen={isOpen} onOpenChange={setIsOpen} size="lg"><ModalContent><ModalHeader>Modal Title</ModalHeader><ModalBody><ModalSizeIndicator />Content...</ModalBody></ModalContent></Modal>
24
Props Overview
Modal Props
PROP | TYPE | DEFAULT | DESCRIPTION |
---|---|---|---|
children | ReactNode | - | Content, usually ModalContent . |
size | ModalSize | 'md' | Modal width/height. See types for options. |
radius | ModalRadius | 'lg' | Border radius of ModalContent . |
shadow | ModalShadow | 'lg' | Shadow of ModalContent . |
backdrop | 'transparent' | 'opaque' | 'blur' | 'opaque' | Backdrop type. |
scrollBehavior | 'inside' | 'outside' | 'outside' | Scroll behavior for overflow content. |
placement | ModalPlacement | 'auto' | Modal position on screen. |
isOpen | boolean | - | Controlled open state. |
defaultOpen | boolean | false | Initial open state (uncontrolled). |
isDismissable | boolean | true | If modal can be closed by backdrop click or escape/back. |
hideCloseButton | boolean | false | Hide default close button in header. |
closeButton | ReactNode | - | Custom close button element. |
animationProps | object | - | Props for Animated.View (reanimated). |
onOpenChange | (isOpen: boolean) => void | - | Callback for open state changes. |
onClose | () => void | - | Callback when modal closes. |
style | StyleProp<ViewStyle> | - | Style for ModalContent wrapper. |
backdropStyle | StyleProp<ViewStyle> | - | Style for the backdrop view. |
isGlass | boolean | false | Apply glassmorphism to the ModalContent panel. |
glassTint | 'default' | 'light' | 'dark' | Theme-derived | Tint for the content panel's glass effect. |
glassIntensity | number | 70 | Intensity (0-100) for the content panel's glass effect. |
ModalContent Props
PROP | TYPE | DESCRIPTION |
---|---|---|
children | ReactNode | Content of the modal dialog. |
style | StyleProp<ViewStyle> | Custom styles for the content container. |
ModalHeader Props
PROP | TYPE | DESCRIPTION |
---|---|---|
children | ReactNode | Content of the header, e.g., title text. |
style | StyleProp<ViewStyle> | Custom styles for the header container. |
ModalBody Props
PROP | TYPE | DESCRIPTION |
---|---|---|
children | ReactNode | Main content of the modal. |
style | StyleProp<ViewStyle> | Custom styles for the body container. |
ModalFooter Props
PROP | TYPE | DESCRIPTION |
---|---|---|
children | ReactNode | Content for the footer, e.g., action buttons. |
style | StyleProp<ViewStyle> | Custom styles for the footer container. |
Styling
The Modal components can be styled using the style
prop available on Modal
(which applies to ModalContent
), ModalContent
, ModalHeader
, ModalBody
, and ModalFooter
.
The Modal
component also accepts a backdropStyle
prop to customize the backdrop view.
The radius
and shadow
props on the main Modal
component control the appearance of the ModalContent
.
If isGlass
is true on the Modal
, the ModalContent
will have a BlurView
as its background, controlled by glassTint
and glassIntensity
. The ModalContent
's backgroundColor
will be transparent.
1
2function App() {
3 const [isOpen, setIsOpen] = useState(false);
4 return (
5 <View>
6 <Button onPress={() => setIsOpen(true)}>Open Styled Modal</Button>
7 <Modal
8 isOpen={isOpen}
9 onOpenChange={setIsOpen}
10 backdropStyle={{ backgroundColor: 'rgba(0, 100, 0, 0.3)' }}
11 style={{ borderColor: 'green', borderWidth: 2 }} // Applies to ModalContent
12 >
13 <ModalContent>
14 <ModalHeader style={{ backgroundColor: '#e0ffe0' }}>
15 Styled Header
16 </ModalHeader>
17 <ModalBody style={{ paddingVertical: 20 }}>
18 <Text>This modal has custom styles.</Text>
19 </ModalBody>
20 <ModalFooter style={{ justifyContent: 'center' }}>
21 <Button onPress={() => setIsOpen(false)}>OK</Button>
22 </ModalFooter>
23 </ModalContent>
24 </Modal>
25 </View>
26 );
27}
Accessibility
The Modal component aims to follow accessibility best practices:
- The underlying React Native
Modal
handles focus trapping and screen reader announcements to some extent. - The
onRequestClose
prop on the native modal is wired to thehandleClose
function, which aids in handling the Android back button for dismissal. - The default close button in
ModalHeader
includes anaccessibilityLabel
. Custom close buttons should also provide appropriate accessibility labels. - Ensure that interactive elements within the modal are accessible and that the modal's purpose is clear to screen reader users, often through a descriptive
ModalHeader
.