diff --git a/README.md b/README.md
index be218a92..84921bd8 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ function MyComponent() {
Open
+
Content
@@ -52,13 +53,13 @@ Additional props:
`closeThreshold`: Number between 0 and 1 that determines when the drawer should be closed. Example: threshold of 0.5 would close the drawer if the user swiped for 50% of the height of the drawer or more.
-`scrollLockTimeout`: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms
+`scrollLockTimeout`: Duration for which the drawer is not draggable after scrolling content inside of the drawer. Defaults to 500ms.
`snapPoints`: Array of numbers from 0 to 100 that corresponds to % of the screen a given snap point should take up. Should go from least visible. Example `[0.2, 0.5, 0.8]`. You can also use px values, which doesn't take screen height into account.
`fadeFromIndex`: Index of a `snapPoint` from which the overlay fade should be applied. Defaults to the last snap point.
-`modal`: When `false`it allows to interact with elements outside of the drawer without closing it. Defaults to`true`.
+`modal`: When `false` it allows to interact with elements outside of the drawer without closing it. Defaults to `true`.
### Trigger
@@ -84,6 +85,10 @@ An optional accessible description to be announced when the dialog is opened. [P
The button that closes the dialog. [Props](https://www.radix-ui.com/docs/primitives/components/dialog#close).
+### Handle
+
+A drag hint (also known as grabber). Shows people that they can drag the drawer to resize it; they can also tap it to cycle through the snap points, and double tap quickly to close the drawer. Set `preventCycle={false}` to stop this default behavior.
+
### Portal
Portals your drawer into the body.
diff --git a/src/context.ts b/src/context.ts
index 4b9113bb..7f5c3075 100644
--- a/src/context.ts
+++ b/src/context.ts
@@ -12,6 +12,7 @@ interface DrawerContextValue {
onNestedRelease: (event: React.PointerEvent, open: boolean) => void;
dismissible: boolean;
isOpen: boolean;
+ isDragging: false;
keyboardIsOpen: React.MutableRefObject;
snapPointsOffset: number[] | null;
snapPoints?: (number | string)[] | null;
@@ -39,6 +40,7 @@ export const DrawerContext = React.createContext({
openProp: undefined,
dismissible: false,
isOpen: false,
+ isDragging: false,
keyboardIsOpen: { current: false },
snapPointsOffset: null,
snapPoints: null,
diff --git a/src/index.tsx b/src/index.tsx
index 16df95c0..dba9e9f0 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -655,6 +655,7 @@ function Root({
onDrag,
dismissible,
isOpen,
+ isDragging,
shouldFade,
closeDrawer,
onNestedDrag,
@@ -672,6 +673,42 @@ function Root({
);
}
+type HandleProps = React.HTMLAttributes & {
+ preventCycle?: boolean;
+};
+
+const Handle = React.forwardRef(({ preventCycle = false, ...rest }, ref) => {
+ const { visible, closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible } =
+ useDrawerContext();
+
+ function handleCycleSnapPoints() {
+ // Prevent accidental taps while resizing drawer
+ if (isDragging || preventCycle) return;
+
+ const isLastSnapPoint = activeSnapPoint === snapPoints?.[snapPoints?.length - 1] ?? null;
+ if (!snapPoints || (isLastSnapPoint && dismissible)) {
+ closeDrawer();
+ return;
+ }
+
+ const nextSnapPoint = snapPoints[snapPoints.findIndex((point) => point === activeSnapPoint) + 1];
+ setActiveSnapPoint(nextSnapPoint);
+ }
+
+ return (
+
+ );
+});
+Handle.displayName = 'Drawer.Handle';
+
const Overlay = React.forwardRef>(
function ({ children, ...rest }, ref) {
const { overlayRef, snapPoints, onRelease, shouldFade, isOpen, visible } = useDrawerContext();
@@ -805,6 +842,7 @@ export const Drawer = {
Root,
NestedRoot,
Content,
+ Handle,
Overlay,
Trigger: DialogPrimitive.Trigger,
Portal: DialogPrimitive.Portal,
diff --git a/src/style.css b/src/style.css
index 336ed701..d83ca70e 100644
--- a/src/style.css
+++ b/src/style.css
@@ -32,6 +32,28 @@
height: 200%;
}
+[vaul-handle] {
+ display: block;
+ opacity: 0.8;
+ margin-top: 4px;
+ margin-left: auto;
+ margin-right: auto;
+ height: 5px;
+ width: 56px;
+ border-radius: 1rem;
+ touch-action: pan-y;
+ cursor: grab;
+}
+
+[vaul-handle]:hover,
+[vaul-handle]:active {
+ opacity: 1;
+}
+
+[vaul-handle]:active {
+ cursor: grabbing;
+}
+
[vaul-overlay][vaul-snap-points='true']:not([vaul-snap-points-overlay='true']):not([data-state='closed']) {
opacity: 0;
}
diff --git a/src/use-snap-points.ts b/src/use-snap-points.ts
index 9747dcd3..8d193839 100644
--- a/src/use-snap-points.ts
+++ b/src/use-snap-points.ts
@@ -102,13 +102,15 @@ export function useSnapPoints({
);
React.useEffect(() => {
- if (activeSnapPointProp) {
- const newIndex = snapPoints?.findIndex((snapPoint) => snapPoint === activeSnapPointProp) ?? null;
+ if (activeSnapPoint || activeSnapPointProp) {
+ const newIndex =
+ snapPoints?.findIndex((snapPoint) => snapPoint === activeSnapPointProp || snapPoint === activeSnapPoint) ??
+ null;
if (snapPointsOffset && newIndex && typeof snapPointsOffset[newIndex] === 'number') {
snapToPoint(snapPointsOffset[newIndex] as number);
}
}
- }, [activeSnapPointProp, snapPoints, snapPointsOffset, snapToPoint]);
+ }, [activeSnapPoint, activeSnapPointProp, snapPoints, snapPointsOffset, snapToPoint]);
function onRelease({
draggedDistance,
diff --git a/test/src/app/with-handle/page.tsx b/test/src/app/with-handle/page.tsx
new file mode 100644
index 00000000..56e7a62d
--- /dev/null
+++ b/test/src/app/with-handle/page.tsx
@@ -0,0 +1,156 @@
+'use client';
+
+import { clsx } from 'clsx';
+import { useState } from 'react';
+import { Drawer } from 'vaul';
+
+const snapPoints = [0, '148px', '355px'];
+
+export default function Page() {
+ const [snap, setSnap] = useState(snapPoints[1]);
+
+ const activeSnapPointIndex = snapPoints.indexOf(snap as string);
+
+ return (
+
+
{activeSnapPointIndex}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{' '}
+
The Hidden Details
+
2 modules, 27 hours of video
+
+ The world of user interface design is an intricate landscape filled with hidden details and nuance. In
+ this course, you will learn something cool. To the untrained eye, a beautifully designed UI.
+
+
+
+
Module 01. The Details
+
+
+ Layers of UI
+ A basic introduction to Layers of Design.
+
+
+ Typography
+ The fundamentals of type.
+
+
+ UI Animations
+ Going through the right easings and durations.
+
+
+
+
+
+
+ “I especially loved the hidden details video. That was so useful, learned a lot by just reading it.
+ Can’t wait for more course content!”
+
+
+ Yvonne Ray, Frontend Developer
+
+
+
+
+
Module 02. The Process
+
+
+ Build
+ Create cool components to practice.
+
+
+ User Insight
+ Find out what users think and fine-tune.
+
+
+ Putting it all together
+ Let's build an app together and apply everything.
+