Skip to contentSkip to navigationSkip to topbar
Figma
Star

Non Modal Dialog Primitive

Version 2.0.2GithubStorybook

An unstyled and accessible base upon which to build a non-modal dialog.


Component preview theme
const NonModalDialogExample = () => {
const nonModalDialog = useNonModalDialogPrimitiveState();
return (
<>
<NonModalDialogDisclosurePrimitive {...nonModalDialog}>✊ Action</NonModalDialogDisclosurePrimitive>
<NonModalDialogPrimitive {...nonModalDialog} aria-label="Welcome" style={{backgroundColor: '#000', zIndex:10}}>
<NonModalDialogArrowPrimitive {...nonModalDialog} />
<span style={{color: '#fff'}}>Black Lives Matter. We stand with the Black community</span>
</NonModalDialogPrimitive>
</>
);
};
render(
<NonModalDialogExample />
)

Guidelines

Guidelines page anchor

About Non-Modal Dialog Primitive

About Non-Modal Dialog Primitive page anchor

The non-modal dialog primitive is an unstyled functional version of a non-modal dialog. Our Popover is built on top of this primitive.

The purpose of providing these unstyled primitives is to cater for instances when the styled Popover provided by Paste doesn’t meet the requirements needed to solve a unique or individual customer problem. At that point you are welcome to fall back to this functional primitive to roll your own styled non-modal dialog while still providing a functional and accessible experience to your customers. However, we strongly recommend reviewing your design proposal with the Design Systems team before doing so.

This primitive should be used to compose all custom non-modal dialogs or popoovers to ensure accessibility and upgrade paths.

(warning)

Before you roll your own non-modal dialogs...

We strongly suggest that all components built on top of this primitive get reviewed by the Design Systems team and go through the UX Review process to ensure an excellent experience for our customers.

Component preview theme
const NonModalDialogExample = () => {
const nonModalDialog = useNonModalDialogPrimitiveState();
return (
<>
<NonModalDialogDisclosurePrimitive {...nonModalDialog}>✊ Action</NonModalDialogDisclosurePrimitive>
<NonModalDialogPrimitive {...nonModalDialog} aria-label="Welcome" style={{backgroundColor: '#000', zIndex:10}}>
<NonModalDialogArrowPrimitive {...nonModalDialog} />
<span style={{color: '#fff'}}>Black Lives Matter. We stand with the Black community</span>
</NonModalDialogPrimitive>
</>
);
};
render(
<NonModalDialogExample />
)
Component preview theme
const RightPlacementExample = () => {
const nonModalDialog = useNonModalDialogPrimitiveState({placement: 'right'});
return (
<>
<NonModalDialogDisclosurePrimitive {...nonModalDialog}>Open non-modal dialog</NonModalDialogDisclosurePrimitive>
<NonModalDialogPrimitive {...nonModalDialog} aria-label="Welcome" style={{backgroundColor: '#000', zIndex:10}}>
<NonModalDialogArrowPrimitive {...nonModalDialog} />
<span style={{color: '#fff'}}>Welcome to Paste!</span>
</NonModalDialogPrimitive>
</>
);
};
render(
<RightPlacementExample />
)

Non-Modal Dialog without Gutter

Non-Modal Dialog without Gutter page anchor
Component preview theme
const GutterExample = () => {
const nonModalDialog = useNonModalDialogPrimitiveState({gutter: 0});
return (
<>
<NonModalDialogDisclosurePrimitive {...nonModalDialog}>Open non-modal dialog</NonModalDialogDisclosurePrimitive>
<NonModalDialogPrimitive {...nonModalDialog} aria-label="Welcome" style={{backgroundColor: '#000', zIndex:10}}>
<span style={{color: '#fff'}}>Welcome to Paste!</span>
</NonModalDialogPrimitive>
</>
);
};
render(
<GutterExample />
)

Styling a Custom Non-Modal Dialog

Styling a Custom Non-Modal Dialog page anchor

The Non-Modal Dialog primitive can be styled using Paste components and tokens.

By using the as prop, we can change the underlying element and combine it with another component. In the example below we're combiningthe NonModalDialogDisclosurePrimitive with the Button component.

We're also using the as prop on the NonModalDialogPrimitive to create a styled Box with Paste token background color, border radius, and spacing values.

Because the non-modal dialog lives outside of the main body tag, we need to provide the base Paste styles using StyledBase from our theme package. This gives the Text primitive the appropriate font stack and sizing.

Component preview theme
const StyledNonModalDialog = React.forwardRef(({children, ...props}, ref) => (
<Box backgroundColor="colorBackgroundBodyInverse" borderRadius="borderRadius20" padding="space30" ref={ref} {...props}>{children}</Box>
));
const StyledExample = () => {
const theme = useTheme();
const nonModalDialog = useNonModalDialogPrimitiveState({placement: 'right'});
return (
<>
<NonModalDialogDisclosurePrimitive {...nonModalDialog} as={Button} variant="secondary">
Open non-modal dialog
</NonModalDialogDisclosurePrimitive>
<NonModalDialogPrimitive {...nonModalDialog} aria-label="Welcome" as={StyledNonModalDialog}>
<StyledBase>
<Text color="colorTextInverse">
Welcome to Paste!
</Text>
</StyledBase>
</NonModalDialogPrimitive>
</>
);
};
render(
<StyledExample />
)

This package is a wrapper around the Reakit Popover(link takes you to an external page). If you’re wondering why we wrapped that package into our own, we reasoned that it would be best for our consumers’ developer experience. For example:

  • If we want to migrate the underlying nuts and bolts in the future, Twilio products that depend on this primitive would need to replace all occurrences of import … from ‘x-package’ to import … from ‘@some-new/package’. By wrapping it in @twilio-paste/x-primitive, this refactor can be avoided. The only change would be a version bump in the ‘package.json` file for the primitive.
  • We can more strictly enforce semver and backwards compatibility than some of our dependencies.
  • We can control when to provide an update and which versions we allow, to help reduce potential bugs our consumers may face.
  • We can control which APIs we expose. For example, we may chose to enable or disable usage of certain undocumented APIs.

Installation

Installation page anchor
yarn add @twilio-paste/non-modal-dialog-primitive - or - yarn add @twilio-paste/core

This props list is a scoped version of the properties available from the Reakit Popover(link takes you to an external page) package.

useNonModalDialogPrimitiveState
useNonModalDialogPrimitiveState page anchor
baseId?: string

Sets the ID that will serve as a base for all the items IDs.

gutter?: string

Sets the offset between the reference and the popover on the main axis.

placement?: "auto-start" | "auto" | "auto-end" | "top-start...

Sets the placement of popover in relation to the NonModalDialogDisclosurePrimitive. Available options include:

  • auto-start
  • auto-end
  • auto
  • top-start
  • top-end
  • top
  • bottom-start
  • bottom-end
  • bottom
  • right-start
  • right-end
  • right
  • left-start
  • left-end
  • left
visible?: boolean

Whether the dialog is visible or not.

animated?: number | boolean

If true, animating will be set to true when visible is updated. It'll wait for stopAnimation to be called or a CSS transition ends. If animated is set to a number, stopAnimation will be called only after the same number of milliseconds have passed.

modal?: boolean

Sets the modal state.

  • Non-modal: preventBodyScroll doesn't work and focus is free.
  • Modal: preventBodyScroll is automatically enabled, focus is trapped within the dialog and the dialog is rendered within a Portal.
hideOnEsc?: boolean | undefined

If enabled, the user can hide the dialog by pressing Escape.

hideOnClickOutside?: boolean | undefined

If enabled, the user can hide the dialog by clicking outside it.

NonModalDialogArrowPrimitive
NonModalDialogArrowPrimitive page anchor
size?: string | number | undefined

The size of the arrow

fill?: string

Sets the fill color of the arrow svg path

stroke?: string

Sets the stroke color of the arrow svg path

NonModalDialogDisclosurePrimitive
NonModalDialogDisclosurePrimitive page anchor
disabled boolean | undefined

Sets the disclosure to disabled.

focusable boolean | undefined

Sets the disclosure to be focusable or not.