├── .commitlintrc.json ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── apps └── www │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── next.config.ts │ ├── package.json │ ├── postcss.config.mjs │ ├── prettier.config.mjs │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon.ico │ ├── og.jpg │ └── site.webmanifest │ ├── scripts │ └── copy-examples.ts │ ├── src │ ├── app │ │ ├── examples │ │ │ └── page.tsx │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components │ │ ├── analytics.tsx │ │ ├── background.tsx │ │ ├── button.tsx │ │ ├── code-dialog.tsx │ │ ├── examples │ │ │ ├── basic.tsx │ │ │ ├── directions.tsx │ │ │ ├── nested.tsx │ │ │ ├── non-dismissable.tsx │ │ │ ├── scaled-background.tsx │ │ │ ├── scrollable.tsx │ │ │ ├── snap-points.tsx │ │ │ └── with-input.tsx │ │ └── logo.tsx │ ├── config │ │ └── site.ts │ ├── constants │ │ └── examples.tsx │ └── lib │ │ ├── get-example-content.ts │ │ └── utils.tsx │ ├── tailwind.config.ts │ └── tsconfig.json ├── changelog.config.js ├── package.json ├── packages └── core │ ├── .gitignore │ ├── README.md │ ├── eslint.config.mjs │ ├── package.json │ ├── prettier.config.mjs │ ├── src │ ├── constants.ts │ ├── context.ts │ ├── hooks │ │ ├── use-controlled.ts │ │ ├── use-fork-ref.ts │ │ ├── use-position-fixed.ts │ │ ├── use-prevent-scroll.ts │ │ ├── use-scale-background.ts │ │ └── use-snap-points.ts │ ├── index.tsx │ ├── style.css │ ├── types │ │ └── index.ts │ └── utils │ │ ├── browser.ts │ │ └── helpers.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── prettier.config.mjs ├── static └── og.jpg └── turbo.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/.npmrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Emil Kowalski 4 | Copyright (c) 2025 Bora Baloğlu 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vaul Base 2 | 3 | ![](./static/og.jpg) 4 | 5 | [![npm version](https://flat.badgen.net/npm/v/vaul-base?color=green)](https://npmjs.com/package/vaul-base) 6 | [![npm downloads](https://flat.badgen.net/npm/dm/vaul-base?color=green)](https://npmjs.com/package/vaul-base) 7 | [![license](https://flat.badgen.net/github/license/borabaloglu/vaul-base?color=green)](https://github.com/borabaloglu/vaul-base/blob/main/LICENSE) 8 | 9 | Vaul Base is an unstyled drawer component for React, built with Base UI. It serves as a replacement for Dialog on mobile and tablet 10 | devices. The component utilizes [Base UI's Dialog](https://base-ui.com/react/components/dialog) internally. 11 | 12 | This is a port of [Vaul](https://vaul.emilkowal.ski/) to Base UI. It's originally created by [Emil Kowalski](https://emilkowal.ski/). 13 | 14 | ## Usage 15 | 16 | To start using Vaul Base, install it in your project: 17 | 18 | ```bash 19 | npm install vaul-base 20 | ``` 21 | 22 | Use the Drawer in your application: 23 | 24 | ```tsx 25 | import { Drawer } from "vaul-base" 26 | 27 | function MyDrawerComponent() { 28 | return ( 29 | 30 | Open Drawer 31 | 32 | 33 | Drawer content 34 | 35 | 36 | ) 37 | } 38 | ``` 39 | 40 | ## Examples 41 | 42 | [Here are additional examples](https://vaul-base.vercel.app/examples) demonstrating the component in use. 43 | 44 | ## Documentation 45 | 46 | You can use original [Vaul documentation](https://vaul.emilkowal.ski/getting-started) for more information. 47 | -------------------------------------------------------------------------------- /apps/www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # examples 44 | public/e 45 | -------------------------------------------------------------------------------- /apps/www/README.md: -------------------------------------------------------------------------------- 1 | # Vaul Base Website 2 | -------------------------------------------------------------------------------- /apps/www/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc" 2 | 3 | const compat = new FlatCompat({ 4 | baseDirectory: import.meta.dir, 5 | }) 6 | 7 | /** @type {import('eslint').Linter.Config[]} */ 8 | const config = [ 9 | ...compat.config({ 10 | $schema: "https://json.schemastore.org/eslintrc", 11 | root: true, 12 | extends: [ 13 | "next/core-web-vitals", 14 | "next/typescript", 15 | "turbo", 16 | "prettier", 17 | "plugin:tailwindcss/recommended", 18 | ], 19 | plugins: ["tailwindcss", "@typescript-eslint"], 20 | ignorePatterns: ["node_modules/", ".next/", "dist/", "build/"], 21 | rules: { 22 | "@next/next/no-html-link-for-pages": "off", 23 | "react/jsx-key": "off", 24 | "tailwindcss/no-custom-classname": "off", 25 | "react/no-unescaped-entities": "off", 26 | }, 27 | settings: { 28 | tailwindcss: { 29 | callees: ["merge"], 30 | config: "./tailwind.config.js", 31 | }, 32 | }, 33 | }), 34 | { 35 | files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], 36 | }, 37 | ] 38 | 39 | export default config 40 | -------------------------------------------------------------------------------- /apps/www/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | } 6 | 7 | export default nextConfig 8 | -------------------------------------------------------------------------------- /apps/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "pnpm copy-examples && next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "copy-examples": "npx tsx scripts/copy-examples.ts" 11 | }, 12 | "dependencies": { 13 | "@base-ui-components/react": "1.0.0-alpha.8", 14 | "@vercel/analytics": "^1.4.1", 15 | "class-variance-authority": "^0.7.1", 16 | "clsx": "^2.1.1", 17 | "lucide-react": "^0.473.0", 18 | "next": "15.1.6", 19 | "react": "^18.0.0", 20 | "react-dom": "^18.0.0", 21 | "shiki": "^2.1.0", 22 | "tailwind-merge": "^2.6.0", 23 | "vaul-base": "workspace:*" 24 | }, 25 | "devDependencies": { 26 | "@eslint/eslintrc": "^3", 27 | "@types/node": "^20", 28 | "@types/react": "^18", 29 | "@types/react-dom": "^18", 30 | "eslint": "^9", 31 | "eslint-config-next": "15.1.6", 32 | "eslint-plugin-tailwindcss": "^3.18.0", 33 | "postcss": "^8", 34 | "tailwindcss": "^3.4.1", 35 | "typescript": "^5" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /apps/www/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/www/prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import baseConfig from "../../prettier.config.mjs" 2 | 3 | const config = { 4 | ...baseConfig, 5 | importOrder: [ 6 | "^(react/(.*)$)|^(react$)", 7 | "", 8 | "", 9 | "^@/src/(.*)$", 10 | "", 11 | "^@/components/(.*)$", 12 | "", 13 | "^@/constants/(.*)$", 14 | "", 15 | "^@/lib/(.*)$", 16 | "", 17 | "^[./]", 18 | ], 19 | } 20 | 21 | export default config 22 | -------------------------------------------------------------------------------- /apps/www/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/www/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/www/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/www/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/favicon.ico -------------------------------------------------------------------------------- /apps/www/public/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/borabaloglu/vaul-base/af41ac2f19d6b24578e9179866ca7bbe54592326/apps/www/public/og.jpg -------------------------------------------------------------------------------- /apps/www/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#09090b", 17 | "background_color": "#09090b", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /apps/www/scripts/copy-examples.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs/promises" 2 | import path from "path" 3 | 4 | async function copyExamples() { 5 | const sourceDir = path.join(process.cwd(), "src/components/examples") 6 | const targetDir = path.join(process.cwd(), "public/e") 7 | 8 | try { 9 | // Create the target directory if it doesn't exist 10 | await fs.mkdir(targetDir, { recursive: true }) 11 | 12 | // Read all files from the source directory 13 | const files = await fs.readdir(sourceDir) 14 | 15 | // Copy each .tsx file 16 | for (const file of files) { 17 | if (file.endsWith(".tsx")) { 18 | await fs.copyFile( 19 | path.join(sourceDir, file), 20 | path.join(targetDir, file) 21 | ) 22 | console.log(`Copied ${file} to public/e`) 23 | } 24 | } 25 | 26 | console.log("Successfully copied all example files") 27 | } catch (error) { 28 | console.error("Error copying examples:", error) 29 | process.exit(1) 30 | } 31 | } 32 | 33 | copyExamples() 34 | -------------------------------------------------------------------------------- /apps/www/src/app/examples/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChevronLeft } from "lucide-react" 4 | import Link from "next/link" 5 | 6 | import { Button } from "@/components/button" 7 | import CodeDialog from "@/components/code-dialog" 8 | 9 | import { EXAMPLES } from "@/constants/examples" 10 | 11 | export default function Examples() { 12 | return ( 13 |
14 |
15 |
16 |
17 | 18 | 22 | 23 |
24 |
25 | 26 |
27 |
28 |

Examples

29 |

30 | Explore different implementations and use cases of the Vaul Base 31 | drawer component. 32 |

33 | 34 |
35 | {Object.entries(EXAMPLES).map(([key, example]) => ( 36 |
40 |

{example.name}

41 |

42 | {example.description} 43 |

44 |
45 | {example.render()} 46 | 47 |
48 |
49 | ))} 50 |
51 |
52 |
53 |
54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /apps/www/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: oklch(0.145 0 0); 8 | --foreground: oklch(0.985 0 0); 9 | --card: oklch(0.145 0 0); 10 | --card-foreground: oklch(0.985 0 0); 11 | --popover: oklch(0.145 0 0); 12 | --popover-foreground: oklch(0.985 0 0); 13 | --primary: oklch(0.985 0 0); 14 | --primary-foreground: oklch(0.145 0 0); 15 | --secondary: oklch(0.205 0 0); 16 | --secondary-foreground: oklch(0.632 0 0); 17 | --muted: oklch(0.237 0 0); 18 | --muted-foreground: oklch(0.556 0 0); 19 | --accent: oklch(0.237 0 0); 20 | --accent-foreground: oklch(0.985 0 0); 21 | --destructive: oklch(0.396 0.141 25.723); 22 | --destructive-foreground: oklch(0.985 0 0); 23 | --danger: oklch(0.22 0.08 26.1); 24 | --danger-foreground: oklch(0.671 0.214 23.774); 25 | --danger-border: oklch(0.671 0.214 23.774); 26 | --warning: oklch(0.24 0.056 52); 27 | --warning-foreground: oklch(0.738 0.173 80.941); 28 | --warning-border: oklch(0.738 0.173 80.941); 29 | --info: oklch(0.24 0.08 268.5); 30 | --info-foreground: oklch(0.665 0.1895 257.22); 31 | --info-border: oklch(0.665 0.1895 257.22); 32 | --success: oklch(0.23 0.055 153); 33 | --success-foreground: oklch(0.627 0.194 149.214); 34 | --success-border: oklch(0.627 0.194 149.214); 35 | --border: oklch(0.32 0 0); 36 | --input: oklch(0.145 0 0); 37 | --ring: oklch(0.405 0 0); 38 | --chart-1: oklch(0.585 0.23 261.348); 39 | --chart-2: oklch(0.577 0.174 149.642); 40 | --chart-3: oklch(0.593 0.277 303.111); 41 | --chart-4: oklch(0.676 0.218 44.36); 42 | --chart-5: oklch(0.577 0.245 27.325); 43 | } 44 | 45 | * { 46 | @apply border-border; 47 | } 48 | 49 | body { 50 | @apply bg-background text-foreground min-h-screen antialiased; 51 | } 52 | 53 | .Root { 54 | @apply isolate; 55 | } 56 | 57 | .container { 58 | @apply px-4 md:px-0 max-w-screen-2xl; 59 | } 60 | 61 | /* Custom Scrollbar Styles */ 62 | * { 63 | scrollbar-width: thin; 64 | scrollbar-color: var(--muted) transparent; 65 | } 66 | ::-webkit-scrollbar { 67 | width: 4px; 68 | } 69 | ::-webkit-scrollbar-track { 70 | background: transparent; 71 | } 72 | ::-webkit-scrollbar-thumb { 73 | background: var(--subtle); 74 | border-radius: 4px; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /apps/www/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { META_THEME_COLOR, siteConfig } from "@/config/site" 2 | import type { Metadata, Viewport } from "next" 3 | import { Geist } from "next/font/google" 4 | 5 | import "./globals.css" 6 | 7 | import { Analytics } from "@/components/analytics" 8 | 9 | const geistSans = Geist({ 10 | variable: "--font-geist-sans", 11 | subsets: ["latin"], 12 | }) 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: siteConfig.name, 17 | template: `%s - ${siteConfig.name}`, 18 | }, 19 | metadataBase: new URL(siteConfig.url), 20 | description: siteConfig.description, 21 | keywords: ["React", "Drawer", "Base UI", "Vaul", "Dialog"], 22 | authors: [{ name: "Bora Baloglu", url: "https://x.com/borabalogluu" }], 23 | creator: "Bora Baloglu", 24 | openGraph: { 25 | type: "website", 26 | locale: "en_US", 27 | url: siteConfig.url, 28 | title: siteConfig.name, 29 | description: siteConfig.description, 30 | siteName: siteConfig.name, 31 | images: [ 32 | { 33 | url: siteConfig.ogImage, 34 | width: 1200, 35 | height: 630, 36 | alt: siteConfig.name, 37 | }, 38 | ], 39 | }, 40 | twitter: { 41 | card: "summary_large_image", 42 | title: siteConfig.name, 43 | description: siteConfig.description, 44 | images: [siteConfig.ogImage], 45 | creator: "@borabalogluu", 46 | }, 47 | icons: { 48 | icon: "/favicon.ico", 49 | shortcut: "/favicon-16x16.png", 50 | apple: "/apple-touch-icon.png", 51 | }, 52 | manifest: `${siteConfig.url}/site.webmanifest`, 53 | } 54 | 55 | export const viewport: Viewport = { 56 | themeColor: META_THEME_COLOR, 57 | } 58 | 59 | export default function RootLayout({ 60 | children, 61 | }: { 62 | children: React.ReactNode 63 | }) { 64 | return ( 65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
{children}
73 |
74 | 75 | 76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /apps/www/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ExternalLink } from "lucide-react" 4 | import Link from "next/link" 5 | import { Drawer } from "vaul-base" 6 | 7 | import { Background } from "@/components/background" 8 | import { Button } from "@/components/button" 9 | import Logo from "@/components/logo" 10 | 11 | export default function Home() { 12 | return ( 13 |
14 | 15 |
16 |
17 | 18 |

Vaul Base

19 |

20 | An unstyled drawer component for React, built with Base UI. 21 |

22 |
23 |
24 | 25 | } 27 | /> 28 | 29 | 30 | 31 | 32 |
33 |

Drawer component for React

34 |

35 | It serves as a replacement for Dialog on mobile and tablet 36 | devices. 37 |

38 |

39 | It is unstyled and features gesture-driven animations. 40 | Originally created by{" "} 41 | 47 | Emil Kowalski 48 | 49 | , it was adapted for Base UI by{" "} 50 | 56 | Bora Baloglu 57 | 58 | . 59 |

60 |

61 | The component utilizes{" "} 62 | 68 | Base UI's Dialog 69 | {" "} 70 | internally. 71 |

72 |

73 | 77 | Here are additional examples 78 | {" "} 79 | demonstrating the component in use. 80 |

81 |
82 |
83 |
84 |
85 |
86 | 102 |
103 |
104 | ) 105 | } 106 | -------------------------------------------------------------------------------- /apps/www/src/components/analytics.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react" 4 | 5 | export function Analytics() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /apps/www/src/components/background.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react" 2 | import clsx from "clsx" 3 | 4 | interface BackgroundProps { 5 | className?: string 6 | } 7 | 8 | export const Background: FC = ({ className }) => { 9 | return ( 10 |
16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /apps/www/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const buttonVariants = cva( 7 | "inline-flex items-center justify-center whitespace-nowrap rounded-md font-medium transition-colors duration-200 outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-primary text-primary-foreground hover:bg-primary/80", 12 | secondary: 13 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 14 | ghost: "text-foreground hover:bg-accent hover:text-accent-foreground", 15 | outline: 16 | "border bg-transparent text-foreground hover:bg-accent hover:text-accent-foreground", 17 | link: "text-foreground hover:underline", 18 | destructive: 19 | "bg-destructive text-destructive-foreground hover:bg-destructive/80", 20 | }, 21 | size: { 22 | sm: "h-8 px-3 text-xs", 23 | md: "h-9 px-4 py-2 text-sm", 24 | lg: "h-10 px-5 py-3", 25 | "icon-sm": "size-8 [&>svg]:size-3", 26 | icon: "size-9 [&>svg]:size-4", 27 | "icon-lg": "size-10 [&>svg]:size-5", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "md", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends VariantProps, 39 | React.ButtonHTMLAttributes {} 40 | 41 | const Button = React.forwardRef( 42 | ({ className, variant, size, ...props }, ref) => ( 43 | 73 | )} 74 | /> 75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 | 83 | {name}.tsx 84 | 85 |
86 | 87 |
88 | 91 | ( 93 | 101 | )} 102 | /> 103 |
104 |
105 | {!code && ( 106 |
107 | 108 |
109 | )} 110 |
114 |
115 |
116 | 117 | 118 | 119 | ) 120 | } 121 | 122 | export default CodeDialog 123 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/basic.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | const BasicDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 | 15 |
16 |

Welcome to the Drawer

17 |

18 | This drawer slides up smoothly from the bottom of your screen. 19 |

20 |

21 | Try interacting with it - you can drag it up and down with your 22 | finger or mouse. The natural gestures make it feel right at home 23 | on mobile devices. 24 |

25 |

26 | When you're done, just swipe down or click outside to dismiss 27 | it. Simple and intuitive! 28 |

29 |
30 |
31 |
32 |
33 | ) 34 | } 35 | 36 | export default BasicDrawer 37 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/directions.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | const DirectionsDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 |
15 |

16 | You are not limited to open the drawer from bottom, you can use{" "} 17 | 18 | top 19 | 20 | ,{" "} 21 | 22 | bottom 23 | 24 | ,{" "} 25 | 26 | left 27 | 28 | , or{" "} 29 | 30 | right 31 | {" "} 32 | directions. 33 |

34 |
35 |
36 |
37 |
38 | ) 39 | } 40 | 41 | export default DirectionsDrawer 42 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/nested.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | const NestedDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 | 15 |
16 |

Nested Drawers

17 |

18 | Nested drawers are perfect for creating hierarchical navigation or 19 | displaying related content in a layered interface. 20 |

21 | 22 | ( 24 | 25 | )} 26 | /> 27 | 28 | 29 | 30 | 31 |
32 |

33 | To create a nested drawer, simply place a{" "} 34 | 35 | Drawer.NestedRoot 36 | {" "} 37 | component inside another drawer's content area. 38 |

39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ) 48 | } 49 | 50 | export default NestedDrawer 51 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/non-dismissable.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { Drawer } from "vaul-base" 3 | 4 | import { Button } from "@/components/button" 5 | 6 | const NonDismissableDrawer = () => { 7 | const [isOpen, setIsOpen] = useState(false) 8 | 9 | return ( 10 | 11 | } 13 | /> 14 | 15 | 16 | 17 |
18 |

Non-dismissible Drawer

19 |

20 | Non-dismissible drawers are perfect for scenarios where users must 21 | complete an action before closing, like forms or important 22 | confirmations. 23 |

24 |

25 | To create a non-dismissible drawer, set the{" "} 26 | 27 | dismissible 28 | {" "} 29 | prop to false on the{" "} 30 | 31 | Drawer.Root 32 | {" "} 33 | component. 34 |

35 | 36 |
37 |
38 |
39 |
40 | ) 41 | } 42 | 43 | export default NonDismissableDrawer 44 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/scaled-background.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | const ScaledBackgroundDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 | 15 |
16 |

Scaled Background

17 |

18 | Notice how the background content scales and transforms as you 19 | drag this drawer. 20 |

21 |

22 | This effect creates a sense of depth and dimension, making the 23 | drawer feel more integrated with your application's 24 | interface. 25 |

26 |

27 | The scaling is achieved by simply adding the{" "} 28 | 29 | shouldScaleBackground 30 | {" "} 31 | prop to the{" "} 32 | 33 | Drawer.Root 34 | {" "} 35 | component. 36 |

37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export default ScaledBackgroundDrawer 45 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/scrollable.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | export const ScrollableDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 | 15 |
16 |

The 7-Minute Conversation

17 |

18 | The 6:47 AM train to London 19 | was unusually quiet that morning. Sarah counted only four other 20 | passengers in her car, all of them hidden behind newspapers or 21 | phones. She had seven minutes until her stop, just enough time to 22 | review her presentation one last time. 23 |
24 |
25 | That's when he sat down across from her. Mid-thirties, wearing a 26 | slightly wrinkled suit and carrying a small potted plant. Of all 27 | the empty seats on the train, he chose this one. Sarah shifted 28 | uncomfortably, trying to signal her disinterest in conversation 29 | through body language. 30 |
31 |
32 | "It's a peace lily," he said, 33 | noticing her glance at the plant. His voice was tired but kind. 34 | 35 | "They're supposed to be impossible to kill, but somehow I 36 | managed it." 37 | {" "} 38 | He smiled weakly.{" "} 39 | 40 | "I'm bringing it to my mother's. She has this way of bringing 41 | things back to life." 42 | 43 |
44 |
45 | Sarah checked her watch.{" "} 46 | 6:48 AM. Six minutes left. 47 | She could give a polite nod and return to her presentation, or... 48 |
49 |
50 | "What happened to it?" she found 51 | herself asking. 52 |
53 |
54 | "Life, I suppose," he replied, 55 | gently touching one of the drooping leaves.{" "} 56 | 57 | "I got it when I started my new job last year. Executive 58 | position, corner office, the whole dream. I thought I could 59 | handle everything - the job, the plant, the relationship..." 60 | {" "} 61 | He trailed off.{" "} 62 | 63 | "Turns out I couldn't even keep a supposedly unkillable plant 64 | alive." 65 | 66 |
67 |
68 | 6:49 AM. Sarah closed her 69 | laptop. Her own corner office suddenly felt less important. 70 |
71 |
72 | "I have a peace lily too," she 73 | said.{" "} 74 | 75 | "Got it three years ago, when I started therapy." 76 | 77 |
78 |
79 | His eyes met hers, curious but not intrusive.{" "} 80 | 81 | "Did it help? The therapy, I mean." 82 | 83 |
84 |
85 | 86 | "It did. It taught me that sometimes things need to die for 87 | better things to grow." 88 | {" "} 89 | She gestured to his plant.{" "} 90 | 91 | "Maybe your lily's just making room for something new." 92 | 93 |
94 |
95 | 6:50 AM. They shared a 96 | moment of understanding that felt deeper than their brief 97 | acquaintance should allow. 98 |
99 |
100 | "I quit yesterday," he said.{" "} 101 | 102 | "The executive job. Couldn't remember the last time I saw my 103 | kids awake." 104 | 105 |
106 |
107 | Sarah felt her throat tighten.{" "} 108 | 109 | "My presentation today... it's for a promotion. More 110 | responsibility, more hours..." 111 | {" "} 112 | She looked at her laptop, then back at him.{" "} 113 | "More dying plants?" 114 |
115 |
116 | 6:51 AM.{" "} 117 | "You know," he said,{" "} 118 | 119 | "my mother doesn't just save plants. She makes this incredible 120 | breakfast on Sunday mornings. My kids call it 'Grandma's 121 | life-fixing pancakes.'" 122 | {" "} 123 | He smiled, a real one this time.{" "} 124 | 125 | "When was the last time you had life-fixing pancakes?" 126 | 127 |
128 |
129 | Sarah couldn't remember. Her weekends had become extensions of her 130 | workweek, filled with emails and preparation for Mondays. 131 |
132 |
133 | "Too long," she admitted. 134 |
135 |
136 | 6:52 AM.{" "} 137 | "Sometimes I wonder," he mused,{" "} 138 | 139 | "if we've got it all backwards. We spend our lives chasing 140 | things that look good on paper, while the real stuff - the 141 | pancakes, the alive plants, the bedtime stories - keeps slipping 142 | away." 143 | 144 |
145 |
146 | Sarah's stop was approaching. Two minutes left. 147 |
148 |
149 | "What will you do now?" she asked. 150 |
151 |
152 | 6:53 AM.{" "} 153 | 154 | "Start small. Save this plant. Make pancakes. Read stories." 155 | {" "} 156 | He paused.{" "} 157 | 158 | "Maybe start a small business, something that matters. I used to 159 | teach before the corporate world caught me." 160 | 161 |
162 |
163 | The train began to slow. Sarah looked at her laptop again, then at 164 | her watch. One minute left. She thought about her presentation, 165 | about corner offices, about peace lilies and pancakes. 166 |
167 |
168 | 6:54 AM. The train stopped. 169 | Sarah stood up, but instead of picking up her laptop, she pulled 170 | out a business card. 171 |
172 |
173 | 174 | "When you start that business," 175 | {" "} 176 | she said, writing her personal email on the back, 177 | 178 | "let me know. I might know some people who need reminding about 179 | what matters." 180 | 181 |
182 |
183 | He took the card, smiled, and nodded. 184 |
185 |
186 | As Sarah stepped onto the platform, she pulled out her phone. She 187 | had just enough time to email her boss before the morning meeting: 188 |
189 |
190 | 191 | "Need to reschedule presentation. Family emergency. It's about a 192 | peace lily." 193 | 194 |
195 |
196 | She walked out of the station into the morning sun, leaving behind 197 | a future she had planned for and walking toward one she couldn't 198 | yet see. 199 |
200 |
201 | 202 | Sometimes seven minutes is all it takes to change everything. 203 | 204 |

205 |
206 |
207 |
208 |
209 | ) 210 | } 211 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/snap-points.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import clsx from "clsx" 3 | import { Drawer } from "vaul-base" 4 | 5 | import { Button } from "@/components/button" 6 | 7 | const snapPoints = ["240px", "400px", 1] 8 | 9 | const SnapPointsDrawer = () => { 10 | const [snapPoint, setSnapPoint] = useState( 11 | snapPoints[0] 12 | ) 13 | 14 | return ( 15 | 20 | } 22 | /> 23 | 24 | 25 | 26 | 27 |
33 |
34 |
35 |

Snap Points

36 |
37 | {typeof snapPoint === "number" ? "100%" : snapPoint} 38 |
39 |
40 |

41 | This drawer has three snap points: 240px, 400px, and full 42 | height. Drag the drawer to see it snap to these positions. 43 |

44 |
45 | 46 |
47 |

What are Snap Points?

48 |

49 | Snap points let users drag a drawer to set positions, enhancing 50 | the user experience with consistent drawer heights. This drawer 51 | can snap to 240px, 400px, and full height. 52 |

53 |
54 | 55 |
56 |

57 | Achievement Unlocked: Full Height! 58 |

59 |

60 | You've discovered all three levels! From the subtle peek at 61 | 240px, through the comfortable view at 400px, all the way to 62 | this majestic full-height mode. 63 |

64 |
65 |
66 |
67 |
68 |
69 | ) 70 | } 71 | 72 | export default SnapPointsDrawer 73 | -------------------------------------------------------------------------------- /apps/www/src/components/examples/with-input.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer } from "vaul-base" 2 | 3 | import { Button } from "@/components/button" 4 | 5 | const WithInputDrawer = () => { 6 | return ( 7 | 8 | } 10 | /> 11 | 12 | 13 | 14 | 15 |
16 |

Drawer with input

17 |

18 | This drawer has an input field. Try typing in the input field. 19 |

20 |