Chakra UIのDrawerは、v3で書き方が根本から変わりました。v2の isOpen と DrawerOverlay を使ったコードは、2026年8月28日公開の最新版 3.37.0 でも型エラーになって動きません。この記事では 3.37.0 の型定義とスロットレシピ、変更履歴から確認できる事実をもとに、v3の最小実装、v2からの移行対応表、placementとsizeの指定値、閉じると入力が消える問題までを整理します。
まとめ:Chakra UI v3 Drawer実装の要点
v3のDrawerは Drawer.Root を頂点とする名前空間コンポーネントで、開閉は open と onOpenChange で制御します。
| 論点 | v3(3.37.0)での結論 |
|---|---|
| 開閉プロパティ | open と onOpenChange(引数はオブジェクト) |
| placement | start / end / top / bottom(既定 end) |
| size | xs / sm / md / lg / xl / full(既定 xs) |
| オーバーレイ・閉じるボタン | Drawer.Backdrop・Drawer.CloseTrigger |
| 閉じたときの中身 | 既定で破棄(unmountOnExit: true) |
| スワイプで閉じる | 非対応(Ark UI の Drawer を使う) |
移行で最初に潰すのは placement="right" です。v2の既定値 right は v3では型が受け付けません。もう一つ見落としやすいのが、閉じるとDrawerの中身がアンマウントされる既定動作で、Drawer内のコンポーネントだけが持っていた入力値は閉じた時点で消えます。
Chakra UIにおけるDrawerとDialogの使い分け
Drawerを選ぶ条件とDialogへ倒す条件
ドロワーは画面の端から滑り込むパネルで、元の画面を残したまま補助的な操作面を重ねるUIです。基準は単純で、元の画面の文脈を保ちたいならDrawer、いま答えないと先へ進めないならDialog。削除確認や決済確定は画面中央のDialog、背後の一覧を見ながら条件を変える絞り込みはDrawerが向きます。UI用語としての定義と一般的な選定基準はドロワーUIとモーダル・ダイアログの違いと使い分けで扱っています。
実装上の土台は同じです。3.37.0 の drawer.js は @ark-ui/react/dialog の Dialog をimportしており、useDrawer も Ark UI の useDialog の再エクスポートです。違うのは画面端へ寄せてスライドさせるレシピだけです。
Drawerを使うべきでない画面
入力項目が多いフォームをDrawerに詰め込む設計は避けてください。既定サイズ xs の content は maxW が 20rem(320px)しかなく、長いラベルや複数の入力欄を横に並べるフォームには狭すぎます。後述のとおり閉じると入力が消える既定動作もあり、size を lg まで上げるくらいなら専用ページに切り出したほうが、戻る操作もURL共有も素直になります。Drawerの中でさらにDrawerを開く多重構造もフォーカス管理が崩れやすい形です。画面全体の構造から考えるなら業務システムの画面デザインとUI設計パターンが判断材料になります。
v3のDrawer構成要素と最小実装コード
Drawer.RootからCloseTriggerまでの部品一覧
3.37.0 の namespace.d.ts がエクスポートする部品は、Root / RootProvider / RootPropsProvider / Trigger / Positioner / Content / Backdrop / Body / Header / Footer / Title / Description / CloseTrigger / ActionTrigger / Context の15個です。Drawer 名前空間からドット記法で参照します。次のコードは、アプリのルートに <ChakraProvider value={defaultSystem}> を置いている前提です。置いていないと、3.37.0 では「ChakraProvider で囲み忘れている」という趣旨の例外が出て描画が止まります。
import { Button, CloseButton, Drawer, Portal } from "@chakra-ui/react"
import { useState } from "react"
export function SettingsDrawer() {
const [open, setOpen] = useState(false)
return (
<Drawer.Root open={open} onOpenChange={(e) => setOpen(e.open)} placement="end" size="md">
<Drawer.Trigger asChild>
<Button variant="outline">設定を開く</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>表示設定</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<p>本文をここに置きます。</p>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">キャンセル</Button>
</Drawer.ActionTrigger>
<Button>保存</Button>
</Drawer.Footer>
<Drawer.CloseTrigger asChild>
<CloseButton size="sm" />
</Drawer.CloseTrigger>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}
Drawer.Trigger に asChild を付けると、自前のボタンへ開閉イベントとARIA属性を引き渡せます。Drawer.ActionTrigger はフッターのキャンセル用で、押すとドロワーが閉じます。3.36.1 以前はこのActionTriggerに type="button" が付かず、<form> の中に置くと押した瞬間にフォームが送信されていました。渡した onClick が無視される不具合もあり、どちらもChakra UIの変更履歴によれば3.37.0で修正されています。Drawer内にフォームを置くなら3.37.0へ上げるのが先です。
Positionerが担う配置とPortalを挟む理由
v2に無かった Drawer.Positioner は配置を受け持つ層です。positioner は position: fixed で幅 100vw・高さ 100dvh を占め、placement に応じて justifyContent と alignItems を切り替えて content を端へ寄せます。
Portal で囲むのは、祖先の transform や overflow: hidden から切り離すためです。祖先に transform があると新しいスタッキングコンテキストができ、Drawer の z-index はその中だけで比べられます。祖先自体の重なり順が固定ヘッダーより低ければ、Drawer の z-index をいくら上げてもヘッダーの下に隠れます。
v2からv3への移行対応表と実際に出る型エラー
部品名・プロパティの対応表
v2(2.10.10)とv3(3.37.0)の型定義を突き合わせた対応です。公式のv3移行ガイドも同じ置き換えを示しています。
| v2(2.10.10) | v3(3.37.0) | 備考 |
|---|---|---|
| Drawer | Drawer.Root | Drawerは名前空間 |
| isOpen / onClose | open / onOpenChange | 引数はオブジェクト |
| DrawerOverlay | Drawer.Backdrop | 名称変更 |
| DrawerCloseButton | Drawer.CloseTrigger | ボタンは自前で置く |
| (なし) | Drawer.Positioner | 配置を担当 |
| DrawerContent ほか3つ | Drawer.Content ほか | フラット名も残存 |
| placement=”right” | placement=”end” | left / right は削除 |
| initialFocusRef | initialFocusEl | ref でなく関数 |
| isFullHeight | Content に height=”100%” | 移行ガイドの指定 |
注意したいのは DrawerContent / DrawerHeader / DrawerBody / DrawerFooter の4つです。v3にもフラットな名前で残っているため、一部のimportだけが通ってしまいます。
v2コードを3.37.0で型検査したときのエラー5件
次のv2記法を、React 19.3.1・TypeScript 5.9.3・@chakra-ui/react 3.37.0 の構成で tsc にかけました。
import { Drawer, DrawerOverlay, DrawerContent, DrawerCloseButton,
DrawerHeader, DrawerBody, useDisclosure } from "@chakra-ui/react"
export function OldDrawer() {
const { isOpen, onOpen, onClose } = useDisclosure()
return (
<Drawer isOpen={isOpen} placement="right" onClose={onClose}>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>メニュー</DrawerHeader>
<DrawerBody>本文</DrawerBody>
</DrawerContent>
</Drawer>
)
}
出力は5件です(ファイル名・行番号・パスは省略)。
error TS2305: Module '"@chakra-ui/react"' has no exported member 'DrawerOverlay'.
error TS2305: Module '"@chakra-ui/react"' has no exported member 'DrawerCloseButton'.
error TS2339: Property 'isOpen' does not exist on type '{ open: boolean; onOpen: () => void;
onClose: () => void; onToggle: () => void; setOpen: Dispatch<SetStateAction<boolean>>; }'.
error TS2604: JSX element type 'Drawer' does not have any construct or call signatures.
error TS2786: 'Drawer' cannot be used as a JSX component.
Its type 'typeof import(".../components/drawer/namespace")' is not a valid JSX element type.
読み解きどころは TS2604 と TS2786 です。v3の Drawer は関数コンポーネントではなく名前空間オブジェクトなので、型の問題に見えても実体は書き方の変更です。逆に DrawerContent 以下のimportは素通りします。型エラーの件数で移行の進み具合を測ると、通ってしまった箇所を見落とします。Drawer を含むimportを全件洗い出してドット記法へ置き換えるのが確実です。Chakra UI全体の変更点はChakra UI v3の使い方・インストール手順とv2移行の注意点にまとめています。
openとonOpenChangeによる開閉制御とuseDisclosure
useDisclosureの戻り値5つと最小の開閉実装
useDisclosure はv3でも残っていますが、3.37.0 の use-disclosure.d.ts が返すのは open / onOpen / onClose / onToggle / setOpen の5つです。v2の isOpen は無くなりました。
import { Button, Drawer, Portal, useDisclosure } from "@chakra-ui/react"
export function CartDrawer() {
const { open, onOpen, onClose, onToggle } = useDisclosure()
return (
<>
<Button onClick={onOpen}>カートを見る</Button>
<Button onClick={onToggle}>切り替え</Button>
<Drawer.Root open={open} onOpenChange={(e) => (e.open ? onOpen() : onClose())}>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>カート</Drawer.Title>
</Drawer.Header>
<Drawer.Body>カートの中身</Drawer.Body>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</>
)
}
onOpenChange が受け取るのは真偽値ではなく { open: boolean } を持つオブジェクトです。v2の感覚で onOpenChange={setOpen} と書くと、TypeScriptでは次のエラーで止まります。JavaScriptでは素通りして状態にオブジェクトが入り、「閉じなくなる」不具合になります。
error TS2322: Type 'Dispatch<SetStateAction<boolean>>' is not assignable to type
'(details: OpenChangeDetails) => void'.
非制御(defaultOpen)と制御の使い分け
開閉状態を親やURLと同期させないなら、stateは不要です。Drawer.Trigger と Drawer.CloseTrigger だけで開閉は完結し、初期表示で開きたいときは defaultOpen を渡します。制御に切り替えるのは、開閉を外部の状態や処理と同期させたいときです。開閉をクエリパラメータへ載せる、未保存の入力を理由に閉じる操作を止める、保存処理の完了を待って閉じる、といった場面が当たります。
Triggerの自作コンポーネント化とページ遷移で閉じない問題
開くボタンを自前のコンポーネントへ切り出した途端に反応しなくなるのは、asChild が子要素へpropsを引き渡す仕組みだからです。子コンポーネントがpropsを下のDOM要素へ渡していないと、クリックハンドラもARIA属性も届きません。propsを受け取って展開する形にします。
function MyTriggerButton(props: React.ComponentProps<typeof Button>) {
return <Button {...props} />
}
ドロワー内のリンクでページを移動しても開いたままになる問題は、Drawerがルーターと連動しないことが原因です。drawer.js のimportは React と Ark UI の Dialog、内部のスタイル関数だけです。ナビゲーション用のドロワーでは、パスの変化を監視して setOpen(false) を呼ぶか、リンクを Drawer.ActionTrigger で包んで押下時に閉じます。
placementとsizeの指定値・既定値と実寸
placementの4値とRTLで左右が入れ替わる理由
placement は start / end / top / bottom の4値で、既定は end です。省略時の見た目はv2と同じ右出しですが、placement="right" と書くと次のエラーになります。
error TS2322: Type '"right"' is not assignable to
type 'ConditionalValue<"start" | "end" | "top" | "bottom" | undefined>'.
left / right が消えたのは、書字方向に追従させるためです。レシピの start は通常 slide-from-left-full、_rtl 条件下では slide-from-right-full のアニメーションを使います。多言語対応の予定が無ければ、start を左、end を右と読み替えれば足ります。
サイズごとのmaxW上限とfullの100dvh
size は6値で既定は xs です。レシピが content に設定する maxW を、3.37.0 の tokens/sizes.js の値に展開すると次のとおりです。maxW は幅の上限なので、画面がそれより狭ければ画面幅に収まります。px はルート文字サイズ16pxの場合の換算です。
| size | 参照トークン | content の maxW(上限) |
|---|---|---|
| xs(既定) | xs | 20rem(320px) |
| sm | md | 28rem(448px) |
| md | lg | 32rem(512px) |
| lg | 2xl | 42rem(672px) |
| xl | 4xl | 56rem(896px) |
| full | - | 100vw・高さ100dvh |
size の名前と参照トークンは一致しません。size=”sm” は sm トークン(24rem)ではなく md(28rem)を使います。上限は xl で、size="2xl" は TS2322 で止まります。
contained と Positioner の padding で画面端から浮かせる指定
真偽値の contained を付けると、positioner に padding 4、content に l3 の角丸が入り、画面端から浮いたパネルになります。余白の量を自分で決めたいなら、公式のDrawerドキュメントにあるとおり Drawer.Positioner に padding を直接渡し、content に rounded を付けます。
スマホは下・PCは右に出すレスポンシブ指定
placement と size はブレークポイントごとのオブジェクトで渡せます。スマホでは下から全幅、md 以上では右から出す絞り込みパネルは次のとおりです。
import { Button, Drawer, Portal } from "@chakra-ui/react"
export function FilterDrawer() {
return (
<Drawer.Root placement={{ base: "bottom", md: "end" }} size={{ base: "full", md: "sm" }}>
<Drawer.Trigger asChild>
<Button variant="outline">絞り込み</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content roundedTop={{ base: "l3", md: "none" }}>
<Drawer.Header>
<Drawer.Title>絞り込み条件</Drawer.Title>
</Drawer.Header>
<Drawer.Body>条件の一覧</Drawer.Body>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}
top / bottom のときレシピは content の maxW を 100% で上書きするので、下出しでは size の幅指定が効きません。角丸も下出しのときだけ上側に付けます。ブレークポイントの切り方はメディアクエリとブレークポイント設計の実装手順が使えます。
モバイル表示で崩れる原因と3.37.0での扱い
100vhと100dvhの違いによる下部の見切れ
スマートフォンで全画面ドロワーの下部が隠れるのは、v2時代の定番のつまずきでした。v2の isFullHeight は型定義のコメントで100vhを占めると説明されており、100vh はアドレスバーが隠れた状態の高さなので表示領域からはみ出します。3.37.0 のレシピは backdrop・positioner・content のいずれも、アドレスバーの伸縮に追従する 100dvh を使います。content の maxH を 100vh で上書きしないでください。
本文だけをスクロールさせるbodyスロットと背景スクロールのロック
レシピの base では body スロットに overflow: auto と flex: 1 が入っており、ヘッダーとフッターを固定したまま本文だけが縦にスクロールします。追加のCSSは要りません。背景のスクロールは preventScroll(既定 true)が止めますが、3.36.1 以前は <html> 側がスクロールするレイアウトでロックが効かず、背後のページが動いていました。3.37.0の変更履歴に修正が載っています。
初期フォーカス・非モーダル・カスタムコンテナの指定
開いた瞬間のフォーカス先とdata-autofocus
開いたときのフォーカス先は、initialFocusEl → data-autofocus の付いた要素 → data-no-autofocus の付いていない最初のフォーカス可能要素 → content 本体、の順に決まります。data-autofocus と data-no-autofocus は3.37.0で追加された属性で、ref を作らずに指定できます。右上の閉じるボタンに先にフォーカスが当たって検索欄に入力できない、という定番の不満は次の2属性で解消します。
<Drawer.Content>
<Drawer.CloseTrigger asChild data-no-autofocus>
<CloseButton size="sm" />
</Drawer.CloseTrigger>
<Drawer.Body>
<Field.Root>
<Field.Label>キーワード</Field.Label>
<Input placeholder="例:請求書" data-autofocus />
</Field.Root>
</Drawer.Body>
</Drawer.Content>
非モーダルのDrawerとカスタムコンテナ
背後の画面を操作できるまま開いておきたいなら modal={false} を渡します。jsdom上で描画すると、positioner に pointer-events: none、content に pointer-events: auto が自動で付き、body のスクロールロックも掛かりませんでした。公式ドキュメントは positioner への pointerEvents="none" の明示も勧めていますが、3.37.0 が使う @zag-js/dialog 1.43.3 は同じ値を自分で付けます。外側をクリックしても閉じないよう closeOnInteractOutside={false} も併用します。
import { Button, CloseButton, Drawer, Portal } from "@chakra-ui/react"
export function MemoDrawer() {
return (
<Drawer.Root modal={false} closeOnInteractOutside={false}>
<Drawer.Trigger asChild>
<Button variant="outline">メモを開く</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>メモ</Drawer.Title>
</Drawer.Header>
<Drawer.Body>背後の一覧も操作できます</Drawer.Body>
<Drawer.CloseTrigger asChild>
<CloseButton size="sm" />
</Drawer.CloseTrigger>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}
ただし公式はアクセシビリティ上の理由で非モーダルを推奨していません。フォーカスが背後へ移れるため、見出しや名前付け、閉じた後のフォーカスの戻し先まで設計しないと、スクリーンリーダーの利用者が現在位置を把握しにくくなります。常時表示の補助パネルなら、Drawerではなく通常のレイアウト内のサイドバーとして組むほうが筋がよい場面です。画面全体ではなく特定の領域の中に出したいときは、<Portal container={ref}> で描画先を差し替え、Backdrop と Positioner に pos="absolute" と boxSize="full" を指定します。
閉じると入力が消える:unmountOnExitとhideMode
3.37.0 の drawer.js は既定値として unmountOnExit: true と lazyMount: true を設定しています。初めて開くまで中身を描画せず、閉じたら中身を破棄する動きです。Drawer内の useState や非制御の input だけで値を持っていると、次に開いたときは初期値に戻ります。親コンポーネントや外部ストアに値を置いている場合は残ります。
中身を残すなら unmountOnExit={false} を渡し、3.37.0で追加された hideMode で隠し方を選びます。jsdom上で開閉した後の content は次のとおりでした。
| 指定 | 閉じた後の content | 中の副作用 |
|---|---|---|
| 既定(unmountOnExit: true) | DOMから削除 | 破棄 |
| unmountOnExit=false(hideMode 既定) | hidden 属性で非表示 | 動き続ける |
| unmountOnExit=false + hideMode=”activity” | display: none | Effectのクリーンアップを実行 |
<Drawer.Root
open={open}
onOpenChange={(e) => setOpen(e.open)}
unmountOnExit={false}
hideMode="activity"
>
...
</Drawer.Root>
"activity" は React 19.2 で安定版になった Activity を使います。Ark UI の実装は React に Activity が無ければ自動で display-none 扱いに戻すので、古い React では指定しても hidden 属性になるだけです。ReactのActivityの解説のとおり、非表示の間は中のEffectのクリーンアップが走り、再表示で再実行されます。ただし動画の一時停止やWebSocketの切断が起きるのは、それをクリーンアップに書いてある場合だけで、DOMに残る video 要素はそのままでは再生を続けます。入力値の保持だけが目的なら hideMode は既定のままで足ります。閉じている間に購読やタイマーを止めたいときに "activity" を選び、停止処理をEffectのクリーンアップに書いてください。なお hideMode だけを指定して unmountOnExit を既定のままにすると、型エラーは出ないのに何も変わりません。中身は閉じた時点で破棄されるからです。
スワイプで閉じるボトムシートが必要な場合の選択肢
Chakra UIのDrawerは、下へスワイプして閉じる操作にも、半分の高さで止めるスナップにも対応していません。ドラッグを扱う部品が無いので、スマホアプリのようなボトムシートが要件なら別の部品を使います。
Chakra UIの開発チームが手がける Ark UI には、ジェスチャー対応の Drawer があります。Ark UIの変更履歴では 5.32.0(2026年2月21日)で旧 BottomSheet を置き換えて追加されました。swipeDirection は up / down / start / end(既定 down)、snapPoints の既定は [1] です。Chakra UI 3.37.0 の依存にも @ark-ui/react 5.39.0 が入っていますが、直接importするなら package.json に明示して入れてください。pnpm では宣言していない依存はimportできません。
import { Drawer } from "@ark-ui/react/drawer"
import { Portal } from "@ark-ui/react/portal"
export function SheetDrawer() {
return (
<Drawer.Root swipeDirection="down" snapPoints={[0.5, 1]}>
<Drawer.Trigger>詳細を開く</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Title>詳細</Drawer.Title>
<p>下へスワイプすると閉じます。</p>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}
Ark UIのDrawerドキュメントではステータスが Preview で、APIは今後も変わりえます。Ark UI はスタイルを持たないヘッドレス部品なので Chakra のレシピは当たらず、見た目は自前です。ほかの選択肢として、Base UIのDrawerもスワイプでの閉じとスナップに対応し、MUI なら SwipeableDrawer が使えます。Base UIの全体像はBase UIとRadix UI・MUIの違いで比べています。
ボトムシートを含むモバイル画面の操作設計から相談したい場合は、UI/UXデザイン支援も扱っています。
slot recipeによるDrawer既定値の一括変更
画面ごとに placement="start" size="md" と書き続けるより、スロットレシピの defaultVariants を上書きして createSystem に組み込むほうが保守が楽です。スロット名は手書きせず drawerAnatomy.keys() から取ると、trigger のような部品の書き漏れを防げます。
import { createSystem, defaultConfig, defineSlotRecipe } from "@chakra-ui/react"
import { drawerAnatomy } from "@chakra-ui/react/anatomy"
const drawerSlotRecipe = defineSlotRecipe({
slots: drawerAnatomy.keys(),
base: {
content: { bg: "gray.50" },
},
defaultVariants: {
size: "md",
placement: "start",
},
})
export const system = createSystem(defaultConfig, {
theme: {
slotRecipes: { drawer: drawerSlotRecipe },
},
})
ChakraProvider の value にこの system を渡すと、個別に placement や size を渡していないDrawerは、左出し(LTRの場合)・最大幅32rem(ルート文字サイズ16pxで512px)が既定になります。v2の extendTheme と components.Drawer.baseStyle は無くなり、指定の単位がコンポーネントからスロットへ変わりました。スタイルを完全に自前で持つ構成ならHeadless UIのReact・Vue版の違いと使い方も判断材料になります。
よくある質問
ドロワーUIとは何ですか?
画面の端から引き出しのように滑り出すパネル型のUIです。元の画面を残したまま、ナビゲーションメニューやカート、絞り込み条件などを重ねて表示します。
v2のDrawerコードはv3でそのまま動きますか?
動きません。3.37.0 でv2記法を型検査すると5件のエラーが出ます。DrawerContent や DrawerBody のimportは通ってしまうので、Drawer関連のimportを全件ドット記法へ置き換えてください。
Chakra UIのDrawerとDialogは何が違いますか?
どちらも Ark UI の Dialog が土台で、違いはスロットレシピだけです。Drawer は画面端へ寄せてスライドさせ、フォーカストラップや modal などのプロパティは共通です。
useDisclosureはv3でも使えますか?
使えます。戻り値は open / onOpen / onClose / onToggle / setOpen の5つで、v2の isOpen はありません。Drawer.Root の onOpenChange はオブジェクトを受け取るので、(e) => setOpen(e.open) のように中の open を読んでください。
Chakra UIのDrawerはスワイプで閉じられますか?
3.37.0 のDrawerは対応していません。ボトムシートが必要なら Ark UI の Drawer(@ark-ui/react/drawer)が swipeDirection と snapPoints を持ちますが、ステータスは Preview で、スタイルは自前です。