はじめに
最近Next.js v15、Tailwind CSS v4、shadcn/ui v2.3を使ったプロジェクトで、コンポーネントの実装中に不思議なレイアウトの問題に直面しました。
この記事では、shadcn/uiのSwitchコンポーネントを使用した際に起きたスクロール関連の問題と、その原因究明から解決までのプロセスを、備忘も兼ねて残しておこうと思います。
発生した問題
フォーム画面内でshadcn/uiのSwitchコンポーネントを使用したところ、フォームコンテナに設定したoverflow-y-scrollが正常に機能せず、コンテンツの表示領域外のページ全体にもスクロールバーが出現してしまいました。
環境情報
問題が発生した環境は以下の通りです:
- Next.js @15.2.3
- shadcn/ui @2.3.0
- TailwindCSS @^4.0.14
問題を再現するためのシンプルなコード例
export default function MyForm() { const [state, formAction] = useActionState(action, { ...initialActionState } as ActionState); ... ... return ( <form action={formAction} className="flex flex-col w-225 gap-10 h-page-screen py-12" > ... ... ... <div className="flex flex-col gap-4"> <h2 className="text-xl/7 font-semibold">スイッチ</h2> <Switch /> </div> </form> ); }
Switchコンポーネントは pnpm dlx shadcn-ui@latest add switch
で追加したものです。
原因の調査
問題解決のためDevToolsで要素を調査すると、以下のことが判明しました:
- Switchコンポーネント内のhidden inputがposition: absoluteスタイルを持っている
- さらに、この要素が画面の非常に下の位置(ページ最下部付近)に配置されている
shadcn/uiのSwitchコンポーネントのコードを確認したところ、シンプルにradix-uiのSwitchコンポーネントをラップしてスタイルを当てているだけでした。
問題は大本の@radix-ui/react-switchの内部実装に関連していました。
Radixはhidden inputを使用して実際のチェックボックス状態を管理していますが、このinput要素がposition: absoluteで配置され、親要素のpositionが設定されていないため、ページ全体を基準として配置されていました。
実際にissueを探してみると、同様の問題が報告されています。 github.com
解決策
この問題を解決するために、Switchコンポーネントを含む親要素にposition: relativeを設定することで、hidden inputの配置基準を変更しました。
export default function EventForm({ event }: { event?: EventDetail }) { ... ... return ( <form action={formAction} className="flex flex-col w-225 gap-10 h-page-screen py-12" > ... ... ... <div className="flex flex-col gap-4"> <h2 className="text-xl/7 font-semibold">スイッチ</h2> + <div className="relative"> <Switch /> + </div> </div> </form> ); }
あるいは、もちろん別途使い回せるラッパーコンポーネントを作成したり、shadcnのSwitch自体を修正する方法もあります。
function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) { return ( + <div className="relative"> <SwitchPrimitive.Root data-slot="switch" className={cn( "peer data-[state=checked]:bg-slate-900 data-[state=unchecked]:bg-slate-200 focus-visible:border-slate-950 dark:data-[state=unchecked]:bg-slate-200/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-slate-200 border-transparent shadow-xs transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50 dark:data-[state=checked]:bg-slate-50 dark:data-[state=unchecked]:bg-slate-800 dark:focus-visible:border-slate-300 dark:dark:data-[state=unchecked]:bg-slate-800/80 dark:border-slate-800", className )} {...props} > <SwitchPrimitive.Thumb data-slot="switch-thumb" className={cn( "bg-white dark:data-[state=unchecked]:bg-slate-950 dark:data-[state=checked]:bg-slate-50 pointer-events-none block size-6 rounded-full transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0 dark:bg-slate-950 dark:dark:data-[state=unchecked]:bg-slate-50 dark:dark:data-[state=checked]:bg-slate-900" )} /> </SwitchPrimitive.Root> + </div> ); }
ちなみに、これならSwitchだけでなくCheckboxなどのinputでも同様のことが起こるのでは?とふと思ったので調べたら案の定issueや解決記事がありました。
全く同じ問題ですね!
github.com
zenn.dev
まとめ
今回の問題は、次の点から発生していました:
- shadcn/uiのSwitchコンポーネント内部で使用されているhidden inputがposition: absoluteで配置されている。
- この要素に対する親要素にposition: relativeが設定されていないため、ページ全体を基準に配置されてしまう。
- 結果として、要素がページ最下部に配置され、overflow-scrollの挙動が不自然になる。
この問題は単純に親要素にposition: relativeを追加するだけで解決できます。
UIライブラリを使用する際は、内部実装の詳細まで理解することで、予期せぬレイアウト問題を効率的に解決できますね。
最後に
IOでは常にエンジニアを積極採用中です!
少しでも興味を持っていただける方がいましたら、是非カジュアル面談からでも是非ご応募ください! よろしくお願いします!