Skip to main content

Overview

Snap points allow your drawer to rest at predefined heights, creating an iOS-style sheet experience. The drawer automatically snaps to the nearest point when released, providing precise control over drawer positioning.

Basic Usage

Snap points are defined as an array of numbers between 0 and 1, where each value represents a percentage of the screen height.
<script>
	import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

	let open = $state(false);
</script>

<Drawer
	bind:open
	snapPoints={[0.25, 0.5, 0.9]}
>
	<DrawerOverlay class="fixed inset-0 bg-black/40" />
	<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
		<DrawerHandle class="mb-8" />
		<h2>Drawer with Snap Points</h2>
		<p>Drag to see snapping behavior at 25%, 50%, and 90%</p>
	</DrawerContent>
</Drawer>
In this example:
  • 0.25 = 25% of screen height
  • 0.5 = 50% of screen height
  • 0.9 = 90% of screen height

Tracking Active Snap Point

Use bind:activeSnapPoint to track and control the current snap position:
<script>
	import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

	let open = $state(false);
	let activeSnapPoint = $state(undefined);
</script>

<Drawer
	bind:open
	snapPoints={[0.25, 0.5, 0.9]}
	bind:activeSnapPoint
>
	<DrawerOverlay class="fixed inset-0 bg-black/40" />
	<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
		<DrawerHandle class="mb-8" />
		<h2>Current position: {activeSnapPoint ? `${activeSnapPoint * 100}%` : 'None'}</h2>
		<p>Drag to see the active snap point value.</p>
	</DrawerContent>
</Drawer>

Programmatic Control

Change the snap point programmatically by updating activeSnapPoint:
<script>
	import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

	let open = $state(false);
	let activeSnapPoint = $state(undefined);
</script>

<Drawer
	bind:open
	snapPoints={[0.25, 0.5, 0.9]}
	bind:activeSnapPoint
>
	<DrawerOverlay class="fixed inset-0 bg-black/40" />
	<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
		<DrawerHandle class="mb-8" />
		<h2>Drawer with Snap Points</h2>
		
		<!-- Programmatically change snap point -->
		<div class="flex gap-2 mt-4">
			<button onclick={() => activeSnapPoint = 0.25}>25%</button>
			<button onclick={() => activeSnapPoint = 0.5}>50%</button>
			<button onclick={() => activeSnapPoint = 0.9}>90%</button>
		</div>
	</DrawerContent>
</Drawer>

Snap Point Change Callback

React to snap point changes with the onSnapPointChange callback:
<script>
	import { Drawer, DrawerOverlay, DrawerContent, DrawerHandle } from '@abhivarde/svelte-drawer';

	let open = $state(false);
	let activeSnapPoint = $state(undefined);
	
	function handleSnapPointChange(point) {
		console.log('Snapped to:', point);
		// Perform actions based on snap point
		if (point === 0.9) {
			console.log('Drawer fully expanded!');
		}
	}
</script>

<Drawer
	bind:open
	snapPoints={[0.25, 0.5, 0.9]}
	bind:activeSnapPoint
	onSnapPointChange={handleSnapPointChange}
>
	<DrawerOverlay class="fixed inset-0 bg-black/40" />
	<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
		<DrawerHandle class="mb-8" />
		<h2>Drawer with Callback</h2>
		<p>Check console when snapping.</p>
	</DrawerContent>
</Drawer>

How Snap Points Work

Initial Snap Point

When the drawer opens, it automatically snaps to the highest snap point (largest value):
/home/daytona/workspace/source/src/lib/components/Drawer.svelte:101-106
if (snapPoints && snapPoints.length > 0) {
  if (activeSnapPoint === undefined) {
    activeSnapPoint = snapPoints[snapPoints.length - 1];
  }
  const snapPos = (1 - activeSnapPoint) * 100;
  drawerPosition.set(snapPos, { duration: 220 });

Finding Nearest Snap Point

When released, the drawer snaps to the nearest point:
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:34-52
function findNearestSnapPoint(currentPos: number): number {
  if (!drawer.snapPoints || drawer.snapPoints.length === 0) {
    return currentPos;
  }

  const currentSnapValue = 1 - currentPos / 100;
  let nearest = drawer.snapPoints[0];
  let minDiff = Math.abs(currentSnapValue - nearest);

  for (const snapPoint of drawer.snapPoints) {
    const diff = Math.abs(currentSnapValue - snapPoint);
    if (diff < minDiff) {
      minDiff = diff;
      nearest = snapPoint;
    }
  }

  return nearest;
}

Dismissing with Snap Points

Dragging beyond the lowest snap point + 30 pixels dismisses the drawer:
/home/daytona/workspace/source/src/lib/components/DrawerContent.svelte:137-149
if (drawer.snapPoints && drawer.snapPoints.length > 0) {
  const nearestSnapPoint = findNearestSnapPoint(pos);
  const snapPos = snapPointToPosition(nearestSnapPoint);

  const lowestSnapPoint = Math.min(...drawer.snapPoints);
  const lowestSnapPos = snapPointToPosition(lowestSnapPoint);

  if (pos > lowestSnapPos + 30) {
    drawer.closeDrawer();
  } else {
    drawer.drawerPosition.set(snapPos);
    drawer.setActiveSnapPoint?.(nearestSnapPoint);
  }
}

Common Patterns

Two-Height Sheet

<Drawer bind:open snapPoints={[0.4, 0.9]}>
	<!-- Peek at 40%, expand to 90% -->
</Drawer>

Three-Height Sheet

<Drawer bind:open snapPoints={[0.25, 0.5, 0.9]}>
	<!-- Small, medium, and large sizes -->
</Drawer>

Bottom Sheet with Peek

<Drawer bind:open snapPoints={[0.15, 0.9]}>
	<!-- Peek at 15%, expand to 90% -->
</Drawer>

Persistent Snap Points

Combine snap points with state persistence to remember the user’s preferred height:
<script>
	import { Drawer, DrawerOverlay, DrawerContent } from '@abhivarde/svelte-drawer';

	let open = $state(false);
	let activeSnapPoint = $state(undefined);
</script>

<Drawer 
	bind:open 
	snapPoints={[0.25, 0.5, 0.9]}
	bind:activeSnapPoint
	persistState={true}
	persistKey="my-snap-drawer"
	persistSnapPoint={true}
>
	<DrawerOverlay class="fixed inset-0 bg-black/40" />
	<DrawerContent class="fixed bottom-0 left-0 right-0 bg-white rounded-t-lg p-4">
		<h2>Position is saved!</h2>
		<p>The snap point will be restored on reload.</p>
	</DrawerContent>
</Drawer>
Learn more about state persistence in the Persistent State guide

Best Practices

Use 2-3 snap points maximum - too many options can feel overwhelming
Make the highest snap point at least 0.85 (85%) for comfortable viewing
Keep the lowest snap point above 0.15 (15%) to ensure visibility
Snap points only work with direction="bottom" and direction="top" drawers

API Reference

Complete Drawer API with snap point props

Persistent State

Save snap point positions across reloads

Build docs developers (and LLMs) love