Drawer
Drawerは、任意のコンテンツを受け取り、画面の上下左右いずれかの端からスライドインで表示できるコンポーネントです。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Drawer from '$lib/components/ui/modules/Drawer.svelte';
import { X } from '@lucide/svelte';
let isOpen = $state(false);
function openDrawer(e) {
e.preventDefault();
e.stopPropagation();
isOpen = !isOpen;
}
</script>
<Button variant="primary" onclick={openDrawer}>Open Drawer</Button>
<Drawer direction="left" bind:open={isOpen}>
{#snippet children()}
<div class="relative flex flex-col justify-between h-full pt-10 pb-6">
<Button class="absolute top-2 right-2" variant="secondary" size="small" tone="ghost" isSquare onclick={() => (isOpen = false)}><X size="1rem" /></Button>
<div class="p-6">
<Label class="!text-lg !font-semibold mb-1.5" for="input">タイトル</Label>
<p class="text-base-foreground-muted text-sm mb-2">ここに補足文が入ります。</p>
</div>
</div>
{/snippet}
</Drawer>
プロパティ
drawerは、以下のプロパティをサポートしています。
| 名前 | 型 | デフォルト値 | 説明 |
|---|---|---|---|
direction |
string |
"left" |
Drawer の表示方向 |
open |
boolean |
false |
Drawer を表示するかどうか |
dismissible |
boolean |
true |
Drawer 外をクリックすると閉じるかどうか |
インストールの手順
以下のコンポーネントのコードを、使いたいプロジェクトにコピー&ペーストします。
パスは実際のプロジェクトの構成にあわせて更新します。
modules/Drawer.svelte
<!--
@component
## 概要
- 画面の上下左右いずれかの端からスライドインすることで、補足情報や操作パネルを表示するコンポーネントです
## 機能
- 開閉可能なdrawerとして使用できます
- 任意のコンテンツを配置できます
## Usage
```svelte
<Drawer direction="left" bind:open={isOpen}>
{@render children?.()}
</Drawer>
```
-->
<script module lang="ts">
import type { Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { cva, type VariantProps } from 'class-variance-authority';
const FLY_DISTANCE = 300;
export const drawerVariants = cva('fixed z-50 bg-base-surface-default border-base-stroke-default overflow-hidden', {
variants: {
/** drawerの方向 */
direction: {
left: 'top-0 left-0 w-[40vw] h-[100vh] border-r rounded-r-lg max-md:w-[70vw]',
right: 'top-0 right-0 w-[40vw] h-[100vh] border-l rounded-l-lg max-md:w-[70vw]',
top: 'top-0 left-0 w-[100vw] h-[40vh] border-b rounded-bl-lg rounded-br-lg max-md:h-[50vh]',
bottom: 'bottom-0 left-0 w-[100vw] h-[40vh] border-t rounded-tl-lg rounded-tr-lg max-md:h-[50vh]',
},
},
defaultVariants: {
direction: 'right',
},
});
export type DrawerVariants = VariantProps<typeof drawerVariants>;
export type DirectionType = 'left' | 'right' | 'top' | 'bottom';
export interface DrawerProps extends DrawerVariants {
/** クラス */
class?: ClassValue;
/** 開閉のフラグ */
open: boolean;
/** drawerの要素外をクリックしたときに閉じるかどうか */
dismissible?: boolean;
/** drawerの方向 */
direction?: DirectionType;
children: Snippet<[]>;
}
</script>
<script lang="ts">
import { fade, fly } from 'svelte/transition';
let { class: className, children, direction = 'left', open = $bindable(false), dismissible = true }: DrawerProps = $props();
let backgroundElement = $state<HTMLElement>();
let drawerVariantsClass = $derived(drawerVariants({ direction, class: className }));
$effect(() => {
if (open) {
document.body.classList.add('overflow-hidden');
document.addEventListener('click', onClickOutside);
document.addEventListener('keydown', onKeyDown);
}
return () => {
document.body.classList.remove('overflow-hidden');
document.removeEventListener('click', onClickOutside);
document.removeEventListener('keydown', onKeyDown);
};
});
// flyアニメーションのパラメータを取得
function getFlyParams(direction) {
if (!direction) return { x: 0, y: 0, duration: 0 };
return {
x: direction === 'left' ? -FLY_DISTANCE : direction === 'right' ? FLY_DISTANCE : 0,
y: direction === 'top' ? -FLY_DISTANCE : direction === 'bottom' ? FLY_DISTANCE : 0,
duration: 300,
};
}
// drawer外をクリックしたときにdrawerを閉じる
function onClickOutside(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
if (!backgroundElement) return;
if (!(e.target instanceof HTMLElement)) return;
if (backgroundElement.contains(e.target) && dismissible) {
open = false;
}
}
// escキーでdrawerを閉じる
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Escape' && dismissible) {
open = false;
}
}
</script>
{#if open}
<div class="fixed inset-0 z-40 bg-base-container-default/50" bind:this={backgroundElement} transition:fade={{ duration: 150 }}></div>
<div class={drawerVariantsClass} transition:fly={getFlyParams(direction)}>
{@render children()}
</div>
{/if}
使い方
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Drawer from '$lib/components/ui/modules/Drawer.svelte';
import { X } from '@lucide/svelte';
let isOpen = $state(false);
function openDrawer(e) {
e.preventDefault();
e.stopPropagation();
isOpen = !isOpen;
}
</script>
<Button variant="primary" onclick={openDrawer}>Open Drawer</Button>
<Drawer direction="left" bind:open={isOpen}>
{#snippet children()}
<div class="relative flex flex-col justify-between h-full pt-10 pb-6">
<Button class="absolute top-2 right-2" variant="secondary" size="small" tone="ghost" isSquare onclick={() => (isOpen = false)}><X size="1rem" /></Button>
<div class="p-6">
<Label class="!text-lg !font-semibold mb-1.5" for="input">タイトル</Label>
<p class="text-base-foreground-muted text-sm mb-2">ここに補足文が入ります。</p>
</div>
</div>
{/snippet}
</Drawer>
サンプル
Default
特に操作が行われていない、デフォルトの状態です。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Drawer from '$lib/components/ui/modules/Drawer.svelte';
import { X } from '@lucide/svelte';
let isOpen = $state(false);
function openDrawer(e) {
e.preventDefault();
e.stopPropagation();
isOpen = !isOpen;
}
</script>
<Button variant="primary" onclick={openDrawer}>Open Drawer</Button>
<Drawer direction="left" bind:open={isOpen}>
{#snippet children()}
<div class="relative flex flex-col justify-between h-full pt-10 pb-6">
<Button class="absolute top-2 right-2" variant="secondary" size="small" tone="ghost" isSquare onclick={() => (isOpen = false)}><X size="1rem" /></Button>
<div class="p-6">
<Label class="!text-lg !font-semibold mb-1.5" for="input">タイトル</Label>
<p class="text-base-foreground-muted text-sm mb-2">ここに補足文が入ります。</p>
</div>
</div>
{/snippet}
</Drawer>
Direction
スライドインする方向を選択できます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Drawer from '$lib/components/ui/modules/Drawer.svelte';
import { X } from '@lucide/svelte';
let openDirection = $state<'top' | 'right' | 'bottom' | 'left'>('left');
let isOpen = $state(false);
const directions = [
{ label: 'Top', direction: 'top' },
{ label: 'Right', direction: 'right' },
{ label: 'Bottom', direction: 'bottom' },
{ label: 'Left', direction: 'left' },
] as const;
// drawerを開く関数
function openDrawer(e, selectDirection: typeof openDirection) {
e.preventDefault();
e.stopPropagation();
openDirection = selectDirection;
if (openDirection === selectDirection) {
isOpen = false;
queueMicrotask(() => {
isOpen = true;
});
}
else {
isOpen = true;
}
}
</script>
<!-- Drawer の方向をボタンで切り替える -->
<div class="flex gap-4 mb-4 flex-wrap">
{#each directions as dir}
<Button variant="primary" onclick={(e) => openDrawer(e, dir.direction)}>
{dir.label}
</Button>
{/each}
</div>
<!-- 動的に方向を切り替える -->
<Drawer direction={openDirection} open={isOpen}>
{#snippet children()}
<div class="relative flex flex-col justify-between h-full pt-10 pb-6">
<Button class="absolute top-2 right-2" variant="secondary" size="small" tone="ghost" isSquare onclick={() => (isOpen = false)}><X size="1rem" /></Button>
<div class="p-6">
<Label class="!text-lg !font-semibold mb-1.5" for="input">タイトル</Label>
<p class="text-base-foreground-muted text-sm mb-2">ここに補足文が入ります。</p>
</div>
</div>
{/snippet}
</Drawer>
With Input,Select
InputやSelectなどの要素と組み合わせることもできます。
<script lang="ts">
import Button from '$lib/components/ui/atoms/Button.svelte';
import Input from '$lib/components/ui/atoms/Input.svelte';
import Label from '$lib/components/ui/atoms/Label.svelte';
import Select from '$lib/components/ui/atoms/Select.svelte';
import Drawer from '$lib/components/ui/modules/Drawer.svelte';
import { X } from '@lucide/svelte';
let isOpen = $state(false);
let selectValue = $state('');
let inputValue = $state('');
let options = [
{ label: '選択肢1', value: 'item1' },
{ label: '選択肢2', value: 'item2' },
{ label: '選択肢3', value: 'item3' },
];
function openDrawer(e) {
e.preventDefault();
e.stopPropagation();
isOpen = !isOpen;
}
</script>
<Button variant="primary" onclick={openDrawer}>Open Drawer</Button>
<Drawer direction="left" bind:open={isOpen}>
<div class="relative flex flex-col justify-between h-full pt-10 pb-6">
<Button class="absolute top-2 right-2" variant="secondary" size="small" tone="ghost" isSquare onclick={() => (isOpen = false)}><X size="1rem" /></Button>
<div>
<div class="flex flex-col p-6">
<Label class="!text-lg !font-semibold mb-1.5">タイトル</Label>
<p class="text-base-foreground-muted text-sm">ここに補足文が入ります。</p>
</div>
<div class="flex flex-col p-6 gap-4">
<div class="flex flex-col">
<Label class="mb-1.5">ラベル</Label>
<Input type="text" placeholder="プレースホルダー" bind:value={inputValue} />
</div>
<div>
<Label class="mb-1.5">ラベル</Label>
<Select {options} placeholder="選択してください" bind:value={selectValue} />
</div>
</div>
</div>
<div class="flex w-full justify-end gap-2 p-6 max-md:flex-col-reverse">
<Button class="max-md:w-full" variant="secondary" onclick={() => (isOpen = false)}>キャンセル</Button>
<Button class="max-md:w-full" variant="primary" onclick={() => (isOpen = false)}>続ける</Button>
</div>
</div>
</Drawer>