商品検索や管理画面で、検索欄の下に長い一覧を表示することがあります。検索欄を常に残せば条件を変えやすくなりますが、その分、一覧を表示できる高さは小さくなります。
このバランスは「どの要素をスクロールさせるか」で変えられます。まず図で見え方の違いを確認し、その後、同じHTMLで実現する方法とPlaywrightでの検証例を紹介します。
現在の実サイトで見るスクロール前後
デュエマクラシック08 カード検索の公開本番を、2026年10月6日にChromeで撮影しました。同じ表示幅の初期表示とスクロール後を比べています。過去の変更前後ではなく、現在の実サイトの動作例です。
モバイル幅は390×844、PC幅は1280×800のviewportです。モバイル幅もPCのChromeで表示幅を変えて撮影しており、実機スマートフォンでの撮影ではありません。どちらの幅でも、一覧を下へスクロールすると検索欄が画面外へ移動し、上部ヘッダーが残る様子を確認できます。
モバイル幅の初期表示
モバイル幅のスクロール後
PC幅の初期表示
PC幅のスクロール後
操作のしやすさと一覧の広さを選ぶ
下の概念図で、スクロール前後を比べます。濃い枠の内側が画面に見える範囲、青い破線がスクロールする範囲です。上向きの矢印は、下へ読み進めたときに内容が動く方向を表しています。
Aでは検索欄がその場に残り、下の一覧だけが動きます。条件を変えやすい一方、一覧に使える範囲は検索欄の下だけです。
Bでは検索欄も一覧と一緒に上へ流れます。検索欄が画面外へ出た後は、ヘッダー直下まで一覧を表示できます。
図では同じだけ下へ進んだ状態を示しています。どちらも商品05まで見えていますが、Bでは商品01と02も同じ画面に残っています。最初から画面が広いのではなく、検索欄が退避した分を一覧に使えるのが違いです。これは仕組みを説明する概念図で、実画面のスクリーンショットや寸法の測定結果ではありません。
どちらを選ぶかは、画面で繰り返す操作を基準に考えます。
-
条件を少しずつ変えて結果を比較するなら、検索欄を残す構造が候補になります
-
一度絞り込んだ結果を長く眺めるなら、検索欄も流す構造が候補になります
後者では条件を変えるために戻る操作が増えます。閲覧領域だけを広げて終わらせず、再検索への戻り方も一緒に設計します。
実装例 同じHTMLで二つの構造を作る
以下は設計の違いを説明するためのサンプルです。ブラウザでの動作確認は未完了のため、利用時には自身の環境で検証してください。
ここでは、ヘッダーと、その下の検索欄・一覧を持つ画面を使います。検索処理を省いた説明用のサンプルです。HTMLは body 内の抜粋で、CSSは後述の共通設定とA・Bのいずれかを読み込みます。スマートフォン向けには head に <meta name="viewport" content="width=device-width, initial-scale=1"> も指定します。
<div class="app">
<header class="header">
商品一覧
<button id="back" type="button">検索に戻る</button>
</header>
<main class="panel" aria-label="商品検索">
<div class="search">
<label for="query">商品名</label>
<input id="query" type="search">
</div>
<ul class="results" aria-label="検索結果"></ul>
</main>
</div>
スクロールできるだけの内容を用意します。次のJavaScriptはHTMLの後に置きます。
const results = document.querySelector('.results');
for (let i = 1; i <= 50; i++) {
const li = document.createElement('li');
li.textContent = '商品 ' + i;
results.append(li);
}
共通のCSSです。Flexboxは、子要素を並べて余った領域を分配できるレイアウト方式です。ここでは縦方向に並べ、.panel がヘッダー以外の高さを使います。100dvh はブラウザの動的な表示領域に合わせる高さの指定です。
* { box-sizing: border-box; }
body { margin: 0; }
.app {
height: 100vh;
height: 100dvh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.header, .search { flex: none; padding: 16px; }
.panel {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
}
.results { margin: 0; padding: 0; list-style: none; }
.results li { min-height: 80px; padding: 16px; }
A 一覧だけをスクロールさせる
共通CSSに、次の指定を追加します。
.panel { overflow: hidden; }
.results {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.results が残りの高さに収まり、その中だけがスクロールします。検索欄はスクロール対象の外にあるため、画面に残ります。この例では position: fixed や position: sticky は使っていません。
B 検索欄と一覧を一緒にスクロールさせる
AのCSSを、次のCSSに置き換えます。AとBを同時に適用しないでください。
.panel { overflow-y: auto; }
.results {
flex: none;
overflow: visible;
}
今度は親の .panel がスクロール対象です。一覧は flex: none で内容に応じた高さを保ち、検索欄と一緒に流れます。ヘッダーは .panel の外にあるので、引き続き画面に残ります。
高さの制約とスクロール対象をセットで見る
overflow-y: auto は、要素から縦にはみ出した内容をスクロールできるようにする指定です。ただし、要素自体が内容に合わせてどこまでも伸びれば、期待する位置にスクロール領域はできません。上の例では .app の高さを制限し、Flexboxで .panel に残りの高さを割り当てています。overflowの仕様説明
min-height: 0 は、縦に並ぶFlexboxの子要素を、内容の高さより小さくできることを明示する指定です。自動の最小サイズは overflow などの条件によって変わるため、常に必須というわけではありません。入れ子のFlexboxで画面全体が伸びる場合は、スクロールさせたい要素だけでなく、その親の高さと最小サイズも確認します。Flexboxの自動最小サイズ
検索への戻り方も変更する
Bでは「検索に戻る」ボタンを、スクロールしないヘッダーに置いています。実際にスクロールしている .panel を先頭へ戻します。Aにも同じボタンを付ける場合は、戻す対象を .results にします。
const panel = document.querySelector('.panel');
const input = document.querySelector('#query');
panel.tabIndex = 0;
document.querySelector('#back').addEventListener('click', () => {
panel.scrollTo({ top: 0, behavior: 'smooth' });
input.focus({ preventScroll: true });
});
scrollTop は、その要素が上端からどれだけスクロールしたかを示す値です。既存の画面でスクロール対象を変える場合は、先頭へ戻す処理だけでなく、スクロール位置の保存・復元や、末尾まで進んだことを検知する処理も確認します。
キーボードだけでもスクロールできるように、Bでは .panel、Aでは .results に tabindex="0" を付けます。入力欄のラベルと、フォーカス位置が分かる見た目も必要です。Bで検索欄へ戻る際は、入力欄へフォーカスも移しています。
Playwrightでは操作後の状態を待つ
Playwrightは、ブラウザを操作して画面の状態を確認できるテストツールです。Bの仕様は、次のように確認できます。
-
一覧上でスクロールすると
.panelのscrollTopが増える -
検索欄がスクロール領域の上端より上へ移動する
-
ヘッダーの位置が変わらない
-
「検索に戻る」で先頭に戻り、入力欄にフォーカスが当たる
以下は、BのHTML/CSS/JavaScriptを組み合わせた画面を /b.html で配信し、Playwright Testの baseURL を設定した場合のテスト例です。
import { test, expect } from '@playwright/test';
test('検索欄が流れ、ボタンで戻れる', async ({ page }) => {
await page.goto('/b.html');
const panel = page.locator('.panel');
const header = page.locator('.header');
const before = await header.boundingBox();
const box = await panel.boundingBox();
if (!box) throw new Error('panel が見つかりません');
// 一覧が見えている領域でホイール操作する
await page.mouse.move(
box.x + box.width / 2, box.y + box.height - 16
);
await page.mouse.wheel(0, 500);
await expect.poll(() =>
panel.evaluate(el => el.scrollTop)
).toBeGreaterThan(0);
// 位置関係を確認する。この例の panel には枠線がない
await expect.poll(() => page.evaluate(() => {
const p = document.querySelector('.panel').getBoundingClientRect();
const s = document.querySelector('.search').getBoundingClientRect();
return s.bottom <= p.top;
})).toBe(true);
expect(await header.boundingBox()).toEqual(before);
await page.getByRole('button', { name: '検索に戻る' }).click();
await expect.poll(() =>
panel.evaluate(el => el.scrollTop)
).toBe(0);
await expect(page.locator('#query')).toBeFocused();
});
mouse.wheel() は、スクロールが終わるまで待つAPIではありません。操作直後に座標を読むと、動く前や途中の状態を判定することがあります。expect.poll() で条件が成立するまで値を繰り返し確認すると、固定時間の待機に頼らず検証できます。wheelの挙動、expect.poll
Aを検証する場合は期待値が変わります。増えるのは .results.scrollTop で、検索欄の座標は変わらないはずです。初期状態だけでなく、スクロール中と検索へ戻った状態まで確認します。
動作と見た目と使いやすさを分けて確認する
座標や scrollTop のテストで確認できるのは、決めた構造どおりに動くことです。画面幅が狭いときの文字の折り返しや、検索条件を開いたときの窮屈さは、スクリーンショットでも確認すると把握しやすくなります。比較する際は、画面サイズ、データ、並び順、スクロール位置をそろえます。
page.screenshot() は画像を保存します。保存した基準画像との差分を自動判定したい場合は、Playwright Testの toHaveScreenshot() などを使います。画像を撮る処理と、自動で差分を判定するテストは区別しておきます。Playwrightの画像比較
どちらのレイアウトが利用者に合うかは、さらに別の確認です。「条件を変えて結果を比較する」「絞り込み後に多くの項目を見る」など、実際の操作を通して判断します。狭い画面、文字を拡大した状態、スマートフォンでキーボードを開いた状態でも、一覧と検索欄の両方にアクセスできるかを確認します。
まとめ
検索欄と一覧の設計では、常時表示する要素に必要な高さと、利用者が繰り返す操作をセットで考えます。
一覧だけをスクロールさせれば検索欄が残り、親をスクロールさせれば検索欄も一緒に流れます。対象を変えたら、先頭への復帰や位置の記録も同じ要素に合わせます。そのうえで、動作をテストし、見た目と使い勝手を確かめると、変更の意図と確認すべき点が明確になります。
背景となった実装例:検索画面のスクロール領域の見直し





