Chakra UI Drawerの使い方|v3の実装とv2からの移行・placement設定を解説
Chakra UIのDrawerは、v3で書き方が根本から変わりました。v2の isOpen と DrawerOverlay を使ったコードは、現行の 3.36.1 では型エラーになって動きません。しかも一部の名前は v3 にも残っているため、エラーの原因が見えにくくなっています。この記事では 3.36.1 の型定義とスロットレシピから確認できる事実だけを使い、v3の最小実装、v2からの移行対応表、placementとsizeの指定値を整理します。
まとめ:Chakra UI v3 Drawer実装の要点
v3のDrawerは Drawer.Root を頂点とする名前空間コンポーネントになり、開閉は open と onOpenChange で制御します。v2の isOpen / onClose の組み合わせは廃止されました。
| 論点 | v3(3.36.1)での結論 |
|---|---|
| 開閉プロパティ | open と onOpenChange |
| placement | start / end / top / bottom(既定 end) |
| size | xs / sm / md / lg / xl / full(既定 xs) |
| オーバーレイ | Drawer.Backdrop |
| 閉じるボタン | Drawer.CloseTrigger |
| useDisclosure | 存続(isOpen → open) |
| 実装基盤 | Ark UI の Dialog |
オーバーレイと閉じるボタンは、v2の DrawerOverlay / DrawerCloseButton が廃止された結果の置き換え先です。移行で最初に潰すべきは placement。v2の既定値 right は v3では受け付けません。以降で、部品構成・移行時に出るエラー・指定値の実寸を順に見ていきます。
Chakra UIにおけるDrawerとDialogの使い分け
Drawerを選ぶ条件とDialogへ倒す条件
ドロワーは画面の端から滑り込むパネルで、元の画面を残したまま補助的な操作面を重ねるUIです。Chakra UIには Dialog も用意されているため、まずどちらを使うかの判断が来ます。
基準は単純です。元の画面の文脈を保ちたいならDrawer、いま答えないと先へ進めないならDialog。削除確認や決済確定のように、選択するまで作業を止めたい場面は画面中央のDialogが適任。絞り込み条件の調整なら、背後の一覧を見ながら操作できるDrawerを選びます。UI用語そのものの定義と一般的な選定基準は、ドロワー・モーダル・ダイアログの違いと使い分けで扱っています。
実装上は両者の距離がさらに近くなりました。3.36.1 の dist/types/components/drawer/index.d.ts を見ると、useDrawer と useDrawerContext は @ark-ui/react/dialog の useDialog / useDialogContext をそのまま再エクスポートしています。v3のDrawerは、Ark UIのDialogにドロワー用のスロットレシピを被せたものです。フォーカストラップやEscキーの挙動を調べたいときは、Dialogの仕様を読めば答えが出ます。
Drawerを使うべきでない画面
Drawerが不向きな場面もはっきりしています。入力項目が多いフォームをDrawerに詰め込む設計は避けてください。既定サイズ xs の content は maxW が 20rem(320px)しかなく、2カラムのフォームは成立しません。size を lg(42rem)まで上げるくらいなら、専用ページに切り出したほうが戻る操作もURL共有も素直になります。
Drawerの中でさらにDrawerを開く多重構造も、フォーカス管理が破綻しやすい形です。業務画面全体の構造から考えたい場合は、業務システムの画面デザインで使うUI設計パターンの整理が判断材料になります。
v3のDrawer構成要素と最小実装コード
Drawer.RootからCloseTriggerまでの部品一覧
3.36.1 の namespace.d.ts がエクスポートしている部品は、Root / RootProvider / RootPropsProvider / Trigger / Positioner / Content / Backdrop / Body / Header / Footer / Title / Description / CloseTrigger / ActionTrigger / Context の15個です。v2のようにフラットな名前を個別importするのではなく、Drawer 名前空間からドット記法で参照します。
以下のコードは ChakraProvider で囲まれている前提です。アプリのルートに <ChakraProvider value={defaultSystem}> を置いていないと、レシピが適用されず素のHTMLとして描画されます。
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 はフッターのキャンセル操作用で、押すとドロワーが閉じます。
Positionerが担う配置とPortalを挟む理由
v2に無かった Drawer.Positioner は、配置計算を専任で受け持つ層です。3.36.1 のスロットレシピを読むと、placement が変えるのは positioner の justifyContent / alignItems と、content 側のアニメーション名です。start と end では positioner の justifyContent が flex-start / flex-end に切り替わり alignItems は stretch、top と bottom はその逆で justifyContent が stretch、alignItems が flex-start / flex-end に変わります。top と bottom ではさらに content の maxW が 100% で上書きされます。
positioner は position: fixed で幅 100vw・高さ 100dvh を占め、その内側で content を寄せる二段構えです。この分離のおかげで、content のサイズ指定と画面上の寄せ位置が干渉しません。Positionerを省いてContentを直接置くと、この寄せ計算が働かなくなります。
Portal で囲むのは、親要素の overflow: hidden や transform の影響からドロワーを切り離すためです。祖先に transform や filter が付いていると新しいスタッキングコンテキストが作られ、backdrop と positioner が zIndex.popover を基準にした計算値を持っていても、その祖先より前面には出られません。body 直下へ逃がすのがPortalの役割です。
v2からv3への移行対応表と実際に出る型エラー
部品名・プロパティの対応表
v2(2.10.10)とv3(3.36.1)の型定義を突き合わせた対応は次のとおりです。
| v2(2.10.10) | v3(3.36.1) | 備考 |
|---|---|---|
| Drawer | Drawer.Root | v3のDrawerは名前空間オブジェクト |
| isOpen | open | Drawer.Root のプロパティ |
| onClose | onOpenChange | 引数は details オブジェクト |
| DrawerOverlay | Drawer.Backdrop | 名称変更 |
| DrawerCloseButton | Drawer.CloseTrigger | 中身のボタンは自前で置く |
| (なし) | Drawer.Positioner | v3で新設・配置を担当 |
| (なし) | Drawer.Trigger | 開くボタンを紐付け |
| DrawerContent / Header / Body / Footer | Drawer.Content / Header / Body / Footer | フラット名も併存 |
| placement=”right” | placement=”end” | left / right を削除 |
| isFullHeight | size=”full” | v2はtop/bottom限定の高さ指定 |
注意したいのは DrawerContent の行です。この4つはv3にもフラットな名前のまま残っており、「一部のimportだけ通ってしまう」状態を生みます。
v2コードを3.36.1でビルドしたときのエラー5件
次のv2記法を、React 19 と TypeScript 5、@chakra-ui/react 3.36.1 の構成で型検査した結果を示します。
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 です。TS2786 の2行目が示すとおり、Drawer の型は namespace モジュールそのものになっています。v3の Drawer が関数コンポーネントではなく export * as Drawer による名前空間オブジェクトへ変わったためで、エラー文面は型の問題に見えても、実体は書き方そのものの変更です。ここで型定義を疑って時間を溶かす移行が少なくありません。
逆に、エラーが出ない部分にも注意が要ります。DrawerContent DrawerHeader DrawerBody のimportは素通り。型エラーの件数だけを頼りに移行の進捗を測ると、通ってしまった箇所を見落とします。確実なのは、Drawer という語のimportを全件洗い出してドット記法へ機械的に置き換えるやり方です。Chakra UI全体の変更点はChakra UIのv3変更点とインストール手順にまとめてあります。
openとonOpenChangeによる開閉制御とuseDisclosure
useDisclosureの戻り値5つと最小の開閉実装
useDisclosure はv3でも残っています。ただし戻り値の形が変わりました。3.36.1 の dist/types/hooks/use-disclosure.d.ts では、返るのは open / onOpen / onClose / onToggle / setOpen の5つです。v2の isOpen は消え、代わりに状態を直接書き換える setOpen が加わりました。
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.Body>カートの中身</Drawer.Body>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</>
)
}
onOpenChange が受け取るのは真偽値ではなく DrawerOpenChangeDetails というオブジェクトです。中の open を読む必要があります。v2の感覚で onOpenChange={setOpen} と書いた場合、TypeScript なら次のエラーで止まります。
error TS2322: Type 'Dispatch<SetStateAction<boolean>>' is not assignable to type
'(details: OpenChangeDetails) => void'.
JavaScriptで書いている場合は素通りし、状態にオブジェクトが入り込みます。型検査のないプロジェクトほど、この一行が「閉じなくなる」不具合の原因になります。
非制御(defaultOpen)と制御の使い分け
開閉状態を親コンポーネントやURLと同期させないなら、Reactのstateを持つ必要はありません。defaultOpen を渡す非制御パターンで十分です。
<Drawer.Root defaultOpen placement="bottom" size="full" contained>
<Drawer.Trigger asChild>
<Button>ヘルプ</Button>
</Drawer.Trigger>
...
</Drawer.Root>
制御パターンに切り替えるべき条件は3つあります。開閉状態をクエリパラメータへ載せたいとき、ドロワー内の未保存入力を理由に閉じる操作を止めたいとき、複数のドロワーやダイアログの排他制御が必要なときです。それ以外で useState と useDisclosure を持ち込むのは、閉じない不具合の温床を自分で作ることになります。
Triggerの自作コンポーネント化とページ遷移で閉じない問題
移行後の詰まりどころは、asChild と開閉状態の置き場所に集中します。ひとつめは、開くボタンを自前のコンポーネントへ切り出した途端に反応しなくなるケースです。asChild は子要素へpropsを引き渡す仕組みなので、子コンポーネントが受け取ったpropsを下のDOM要素へ透過していないと、クリックハンドラもARIA属性も届きません。propsを無視する自作コンポーネントを置いてSSR出力を比べると、透過版のbuttonには data-part="trigger" aria-haspopup="dialog" aria-expanded="false" data-state="closed" が付くのに対し、非透過版はこれらがすべて欠けた素のbuttonになります。対処は単純で、propsを受け取って展開する形にします。
function MyTriggerButton(props: React.ComponentProps<typeof Button>) {
return <Button {...props} />
}
ふたつめは、ドロワー内のリンクを踏んでページを移動しても開いたままになるケースです。3.36.1 の drawer.js がimportしているのは react/jsx-runtime、@ark-ui/react/dialog、react、内部のstyled-systemだけで、ルーターと連動する仕組みは持ちません。クライアント遷移でコンポーネントがアンマウントされない構成なら、open に入れた値はそのまま残ります。ナビゲーション用のドロワーでは、遷移先のパスを監視して閉じる処理を自分で書くか、開閉状態をURLのクエリパラメータ側に持たせて遷移で自然に落ちるようにします。
placementとsizeの指定値・既定値と実寸
placementの4値とRTLで左右が入れ替わる理由
v3の placement が受け付ける値は start / end / top / bottom の4つで、既定値は end です。v2の既定は right でしたから、指定を省いた場合の見た目は同じ右側になります。ただし placement="right" と書くとエラーになります。
error TS2322: Type '"right"' is not assignable to
type 'ConditionalValue<"start" | "end" | "top" | "bottom" | undefined>'.
正確に言えば、start / end はv2の型定義にも含まれていました。v3で消えたのは left / right のほう。論理方向へ一本化したのは、書字方向に追従させるためです。3.36.1 のレシピを見ると、start のアニメーション名は通常時が slide-from-left-full、_rtl 条件下では slide-from-right-full。アラビア語やヘブライ語のようなRTL言語でも、ドロワーは「本文の始まり側」から出ます。多言語対応の予定がないプロジェクトなら、start を左、end を右と読み替えれば足ります。
サイズごとのmaxW実寸とfullの100dvh
size の指定値は xs / sm / md / lg / xl / full の6つ、既定は xs です。レシピが content に設定する maxW をトークン値に展開すると次の実寸になります。
| size | content の maxW | ピクセル換算 |
|---|---|---|
| xs(既定) | 20rem | 320px |
| sm | 28rem | 448px |
| md | 32rem | 512px |
| lg | 42rem | 672px |
| xl | 56rem | 896px |
| full | 100vw(高さ 100dvh) | 画面全体 |
size の名前とトークン名は一致しません。size=”sm” が参照するのは sm(24rem)ではなく md(28rem)のトークンです。v2の感覚で「smなら24rem」と見積もると幅がずれます。なお上限は xl の 56rem で、2xl のような値を渡すと「”sm” | “md” | “lg” | “xl” | “xs” | “full” に代入できない」という TS2322 で止まります。それ以上の幅が要るなら、Drawerではなく専用ページを検討する場面です。
containedで画面端に張り付かせない指定
v3で追加された contained は真偽値の変数です。付けると positioner に padding 4 が入り、content に l3 の角丸が付きます。画面端まで伸ばさない、余白を残した浮いたパネル向けの指定です。カード風のUIで統一しているアプリなら、これを既定にしておくと個別指定が減ります。
モバイル表示で崩れる原因と3.36.1での扱い
100vhと100dvhの違いによる下部の見切れ
スマートフォンで全画面ドロワーを開くと下部が隠れる。v2時代の定番のつまずきでした。原因は高さの単位です。v2の isFullHeight は型定義のコメントで「placement が top か bottom のとき、ビューポート高(100vh)を占める」と説明されていました。100vh はモバイルブラウザのアドレスバーが隠れる前提の高さを返すため、実際の表示領域からはみ出します。
3.36.1 のレシピは backdrop・positioner・content のいずれも 100dvh を使います。dvh は動的ビューポート高で、アドレスバーの伸縮に追従する単位です。v3では isFullHeight というプロパティ自体が無くなり、全画面は size="full"(maxW 100vw・高さ 100dvh)が担います。
本文だけをスクロールさせるbodyスロットの既定値
内部がスクロールできないという症状も、v3では既定のスタイル側で手当てされています。レシピの base では body スロットに overflow: auto と flex: 1、content に maxH: 100dvh が入っています。ヘッダーとフッターを固定したまま本文だけが縦スクロールする構造は、追加のCSSなしで成立する設計です。
v2向けの記事にある「max-heightとoverflowを自分で当てる」対処は、v3では不要です。むしろ content の maxH を利用側から 100vh へ上書きすると、せっかくのdvh対応を100vhへ戻すことになります。
top・bottom配置で効かなくなるmaxWの調整
残る調整点は content の横幅です。placement が top / bottom のときだけ、レシピは maxW を 100% で上書きします。左右から出す場合は、size の maxW がそのまま効く形。狭い端末では size=”full” にするか、幅をブレークポイントで切り替える指定が要ります。切り替えの設計はメディアクエリとブレークポイントの設計手順の考え方がそのまま使えます。
slot recipeによるDrawer既定値の一括変更
画面ごとに placement="start" size="md" と書き続けるのは保守が面倒です。v3ではスロットレシピの 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 を渡せば、アプリ全体のDrawerが左出し・幅512px(size md が参照する lg トークン32rem)で揃います。v2の extendTheme と components.Drawer.baseStyle という書き方は無くなりました。指定の単位がコンポーネントからスロットへ変わった点が、移行時に読み替えの必要な箇所です。
Chakra UIのレシピを離れ、スタイルを完全に自前管理するヘッドレス構成へ切り替える選択もあります。判断材料としてはBase UIとRadix UI・MUIの違いの比較が参考になります。
よくある質問
ドロワーUIとは何ですか?
画面の端から引き出しのように滑り出してくるパネル型のUIです。元の画面を覆い隠さずに残したまま、補助的な情報や操作を重ねて表示します。代表例はハンバーガーメニューのナビゲーション、ECサイトのカート確認、一覧画面の絞り込み条件パネル。Chakra UI v3では Drawer.Root を頂点とする部品群として提供され、内部実装はArk UIのDialogを共有しています。
v2のDrawerコードはv3でそのまま動きますか?
動きません。@chakra-ui/react 3.36.1 に対してv2記法を型検査すると、DrawerOverlay と DrawerCloseButton のエクスポートが無いこと、useDisclosure に isOpen が無いこと、Drawer をJSXコンポーネントとして使えないことで合計5件のエラーが出ます。厄介なのは DrawerContent や DrawerBody のimportは通ってしまう点です。エラーが消えた時点で移行完了と判断せず、Drawer関連のimportを全件ドット記法へ置き換えてください。
Chakra UIのDrawerとDialogは何が違いますか?
実装の土台は同じです。3.36.1 の型定義では、Drawer の useDrawer と useDrawerContext が Ark UI の Dialog が持つ useDialog / useDialogContext の再エクスポートとして定義されています。違いは適用されるスロットレシピにあり、Drawer は positioner を画面端へ寄せてスライドのアニメーションを当てます。フォーカストラップやEscキーでの閉じる挙動といった土台の仕様は共通です。
useDisclosureはv3でも使えますか?
使えます。ただし戻り値が変わりました。v3では open / onOpen / onClose / onToggle / setOpen の5つが返ります。v2の isOpen は存在しないため、分割代入をそのまま残すと Property ‘isOpen’ does not exist というエラーになります。また Drawer.Root の onOpenChange はオブジェクトを受け取るので、中の open プロパティを読んで onOpen と onClose を呼び分ける形にしてください。
placementにrightを指定するとどうなりますか?
型エラーになります。v3が受け付けるのは start / end / top / bottom の4値で、right や left は含まれません。右から出したい場合は end を指定します。既定値も end なので、v2で placement を省略していたコードは、見た目としては同じ右出しのまま移行できます。left / right が削除されたのは、RTL言語で左右が自動的に入れ替わる論理方向へ一本化するためでした。