リサイザブル
stimeo--resizable
境界をドラッグして領域の広さを変えます。キーボードでも動かせます。
画面を仕切り、その境界をドラッグして左右または上下の広さを変えます。ドラッグ中はポインタを掴んだままなので、境界から外れても画面の外まで行っても、手が離れません。狭くできる限界と広くできる限界は決められます。境界をダブルクリックすると畳んだり戻したりできます。キーボードでも同じことができ、矢印キーで少しずつ、Home と End で両端へ動かせます。今の比率は CSS から読めるので、マークアップとレイアウトの作り方は自由です。
実行中
左ペイン
メインのエディタ領域
右ペイン
プレビュー作業領域
キーボード操作
| キー | 動作 |
|---|---|
| ← / ↑ | スプリッターの比率を 10 段階(あるいは 1% 単位)減らし、左(上)側ペインを縮小する。 |
| → / ↓ | スプリッターの比率を 10 段階(あるいは 1% 単位)増やし、左(上)側ペインを拡大する。 |
| Home | スプリッターの比率を許容される最小比率(minValue)に即座に変更する。 |
| End | スプリッターの比率を許容される最大比率(maxValue)に即座に変更する。 |
| Double Click | スプリッターをダブルクリックすると、ペインを折りたたみ(しきい値以下に縮小)、再ダブルクリックで元の比率に復元する。 |
| F6 | ペイン間をフォーカス巡回する(端で循環)。 |
<%# Markup for the resizable (splitter / pane-split) demo.
stimeo--resizable provides PointerCapture dragging, a double-click collapse
toggle, and keyboard adjustment, and exposes the ratio (0..1) on the root element
as the CSS variable --stimeo--resizable-fraction. %>
<div class="resizable-demo-container">
<div
class="resizable-demo"
id="resizable-splitter"
data-controller="stimeo--resizable"
data-stimeo--resizable-min-value="20"
data-stimeo--resizable-max-value="80"
data-stimeo--resizable-value-value="50"
data-stimeo--resizable-step-value="5"
style="--stimeo--resizable-fraction: 0.5;"
>
<div
class="resizable-demo__pane resizable-demo__pane--left"
id="resizable-demo-primary"
data-stimeo--resizable-target="primary"
>
<div class="resizable-demo__pane-content">
<h3 id="resizable-demo-primary-label">
<%= t("components.resizable.demo.pane_left") %>
</h3>
<p><%= t("components.resizable.demo.pane_left_body") %></p>
</div>
</div>
<div
class="resizable-demo__handle"
role="separator"
tabindex="0"
<%# Window Splitter names the splitter after the primary pane. That pane
has a visible heading here, so the pattern asks for aria-labelledby
rather than a hand-written aria-label. %>
aria-labelledby="resizable-demo-primary-label"
aria-controls="resizable-demo-primary"
aria-orientation="vertical"
data-stimeo--resizable-target="separator"
data-action="
pointerdown->stimeo--resizable#onPointerDown
keydown->stimeo--resizable#onKeydown
dblclick->stimeo--resizable#toggle
"
>
<div class="resizable-demo__handle-bar" aria-hidden="true"></div>
</div>
<div
class="resizable-demo__pane resizable-demo__pane--right"
data-stimeo--resizable-target="secondary"
>
<div class="resizable-demo__pane-content">
<h3><%= t("components.resizable.demo.pane_right") %></h3>
<p><%= t("components.resizable.demo.pane_right_body") %></p>
</div>
</div>
</div>
<div class="resizable-demo__controls">
<%# demo.js (a module) wires this up with addEventListener. Inline on*
attributes can't reference a module-scoped function, so they aren't used. %>
<button
class="demo-trigger resizable-demo__control-btn"
type="button"
data-resizable-toggle-direction>
<%= t("components.resizable.demo.toggle_direction") %>
</button>
</div>
</div>
/*
* Presentation-only styles for the resizable demo.
* The split ratio is expressed by the Playground as CSS Grid column/row widths using
* the --stimeo--resizable-fraction variable (set on the root by the library).
*/
.resizable-demo-container {
display: flex;
flex-direction: column;
gap: 1rem;
width: 100%;
}
.resizable-demo {
display: grid;
/* Bind the ratio variable to the Grid column. */
grid-template-columns: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
grid-template-rows: 1fr;
width: 100%;
height: 250px;
background: var(--surface-subtle);
border: 1px solid var(--border-default);
border-radius: 12px;
overflow: hidden;
box-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
}
/* Vertical-split class toggle. */
.resizable-demo--vertical {
grid-template-columns: 1fr;
/* Bind the ratio variable to the Grid row. */
grid-template-rows: calc(var(--stimeo--resizable-fraction, 0.5) * 100%) 8px 1fr;
}
.resizable-demo__pane {
background: var(--surface-card);
overflow: hidden;
}
.resizable-demo__pane--left {
background: var(--surface-card);
}
.resizable-demo__pane--right {
background: var(--surface-subtle);
}
.resizable-demo__pane-content {
padding: 1.25rem;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
.resizable-demo__pane-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1rem;
color: var(--fg);
}
.resizable-demo__pane-content p {
margin: 0;
font-size: 0.875rem;
color: var(--color-text-muted);
}
/* Splitter handle. */
.resizable-demo__handle {
display: flex;
align-items: center;
justify-content: center;
background: var(--border-strong);
cursor: col-resize;
position: relative;
transition: background 0.15s ease;
}
.resizable-demo--vertical .resizable-demo__handle {
cursor: row-resize;
}
.resizable-demo__handle:hover,
.resizable-demo__handle:focus {
background: var(--accent);
}
.resizable-demo__handle:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.resizable-demo__handle-bar {
width: 2px;
height: 24px;
background: var(--surface-card);
border-radius: 1px;
}
.resizable-demo--vertical .resizable-demo__handle-bar {
width: 24px;
height: 2px;
}
.resizable-demo__controls {
display: flex;
justify-content: flex-end;
}
/* Only the smaller, lighter label is the demo's own; the look is the shared launcher. */
.resizable-demo__control-btn {
font-size: 0.875rem;
font-weight: 500;
}
// Demo script that toggles the split layout direction (horizontal / vertical).
// This file loads as a module (type="module"), so top-level functions are not
// global — wire interactions with addEventListener, not an inline onclick.
const toggleButton = document.querySelector("[data-resizable-toggle-direction]");
toggleButton?.addEventListener("click", () => {
const el = document.getElementById("resizable-splitter");
if (!el) return;
const isVertical = el.classList.toggle("resizable-demo--vertical");
const separator = el.querySelector('[role="separator"]');
if (separator) {
// Horizontal split (column layout): the divider is "vertical".
// Vertical split (row layout): the divider is "horizontal".
separator.setAttribute("aria-orientation", isVertical ? "horizontal" : "vertical");
}
});
これらのスタイルは共通のデザイントークン(ライト/ダーク両対応)を使います。共通スタイルも一緒にコピーし、ルート要素の data-theme を切り替えればダークになります。
このコンポーネントを動かすために HTML へ記述する data-* 属性です。ルート要素に下の data-controller を付け、その内側に各 target / value / action を配置します。
ルート要素に付与
data-controller="stimeo--resizable"
ターゲット
| 名前 | 説明 | 属性 |
|---|---|---|
primary
必須
|
分割比でサイズが決まる主ペイン。 | data-stimeo--resizable-target="primary" |
secondary
必須
|
残り領域を占める副ペイン。 | data-stimeo--resizable-target="secondary" |
separator
必須
|
ドラッグ可能な分割ハンドル(role=separator)。aria-value*状態を持つ。 |
data-stimeo--resizable-target="separator" |
値(Values)
| 名前 | 説明 | 属性 |
|---|---|---|
min
|
分割割合の最小値(%、既定0)。読めない宣言は既定値へ落ちる。 | data-stimeo--resizable-min-value |
max
|
分割割合の最大値(%、既定100)。min を下回る値は min へ潰す。 | data-stimeo--resizable-max-value |
step
|
矢印キー調整の刻み幅(%、既定1)。 | data-stimeo--resizable-step-value |
value
|
現在の分割割合。min/maxで丸められる(%、既定50)。 | data-stimeo--resizable-value-value |
アクション
| 名前 | 説明 | アクション |
|---|---|---|
onKeydown
|
矢印キー(向きに応じて)で分割を調整、Home/Endでmin/max、Enterでトグルする。 | stimeo--resizable#onKeydown |
onPointerDown
|
セパレーターのポインタードラッグを開始し、capture取得とフォーカスを行う。 | stimeo--resizable#onPointerDown |
toggle
|
ペインを min まで折りたたむ、または折りたたむ直前の位置へ戻す(直前位置が無いときだけ max まで開く)。 | stimeo--resizable#toggle |
イベント
| 名前 | 説明 | イベント |
|---|---|---|
change
|
分割値が変わると発火する。detailにvalueとfractionを含む。 | stimeo--resizable:change |
状態フック
ライブラリが操作するのはこれらの ARIA / data 属性、カスタムプロパティだけです。見た目は利用側 CSS がこれらに反応して作ります([aria-selected] / [aria-expanded] / var(--stimeo--…) などのセレクタでフックします)。
| フック | 対象 | 意味 |
|---|---|---|
aria-valuenow |
スプリッター要素 (handle) | 現在の左(上)側ペインのパーセンテージ比率 (0 - 100) がリアルタイムに格納される。 |
aria-valuemin |
スプリッター要素 (handle) | スプリッターの許容最小比率(minValue)を示す。 |
aria-valuemax |
スプリッター要素 (handle) | スプリッターの許容最大比率(maxValue)を示す。 |
--stimeo--resizable-fraction |
ルート要素 (resizable) | 左(上)側ペインが全体に占める比率が "0.0" から "1.0" の小数値として同期される。CSS の grid-template-columns 等で直接ペイン幅としてバインドされる。 |