MONET(自作の会話AIで、中心に立って他のエージェントの指揮をする役)のUIに、右から出てくるサイドパネルがある。
チャットで「今日のタスク見せて」と言うと、右側にスッとパネルが開いてタスク一覧が出てくる。閉じると画面いっぱいにチャットが戻る。Claude Desktopの右サイドバーに近い挙動を、自分のアプリでもやりたかった。
見た目は単純。でもこの「スッと開いて、スッと閉じる」が、実装してみるとぜんぜん気持ちよく動かなかった・・・。
最初の実装はFlexboxで組んでた。
左にサイドバー、真ん中にチャット、右にパネル。パネルは position: absolute で上からかぶせて、transform: translateX() でスライドインさせる。よくあるやり方。
で、これだと何が起きるか。パネルが開いたとき、チャットエリアの右端がパネルの下に潜る。潜ったままだと文章が見切れるから、チャット側も幅を縮めたい。flex: 1 の値を動かして縮めようとする。
ここで引っかかる。Flexboxの flex: 1 はトランジションで滑らかに動かない。width みたいに0.25秒かけて縮んでくれない。パネルのスライドインは滑らかなのに、チャット側はガクッと幅が変わる。境目に一瞬隙間ができたり、チャット側の要素が急に消えたりする。
見た目が気持ち悪い。
途中で requestAnimationFrame の手動アニメーションも考えたし、transform: scale() でごまかすことも考えた。でもどれも本質的じゃない。パネルが450px開いたら、チャットはきっちり450px縮まないとダメ。両者が同じ時間、同じイージングで動かないと、どこかがズレる。
答えはCSS Gridの grid-template-columns だった。
コーディングしてくれている Claude Code(以降 Code)が「grid-template-columns 自体をアニメーションできる」と返してきて、初めて知った。
.container {
display: grid;
grid-template-columns: 260px 1fr 0px;
transition: grid-template-columns 0.25s ease-out;
}
.container.with-panel {
grid-template-columns: 260px 1fr 450px;
}
3カラム構成。左がサイドバー(固定260px)、真ん中がチャット(残り全部)、右がパネル(閉じてるとき0px、開くと450px)。
with-panel クラスがつくと、右のカラムが0pxから450pxに0.25秒かけて伸びる。伸びるぶんだけ真ん中の 1fr が自動で縮む。1行のCSS宣言で、3エリアが連動して動く。
ただ、Gridに切り替えると、Flexbox前提で書いてた子要素の指定がいろいろ壊れる。
サイドバーには width: 260px と書いてあった。これを残すとGridの 260px 1fr 450px と二重指定になって、中身がはみ出したときにGridが勝手に広がる。Gridの原則は親が幅を決めること、子に幅を指定しない、とCode。サイドバーの width は削った。
チャットエリアの内部も grid-template-rows: auto 1fr auto に書き換えてもらって、ヘッダー・チャット本体・入力エリアを縦に並べた。ここで重要なのが min-width: 0 らしい。Gridの子要素はデフォルトで min-width: auto になっていて、中身の最小幅で親を押し広げようとする。これを 0 にしないと、長いコードブロックが出たときにレイアウトが崩れる、とのこと。それと、全Grid子要素に overflow: hidden。忘れると子要素の中身がはみ出してGridが広がって、アニメーション中にカラム幅がブレる。
もう一つ、実装して初めて気づいた罠。
パネルが閉じてるとき、右カラムは0pxだからDOM上は存在しなくていい、と思ってReact側で return null を返してた。するとGridのカラムが3つから2つになって、開けたときのアニメーションが機能しない。
if (!panel) {
return <div className="agent-panel closed" />;
}
空のdivを返すようにCodeに直してもらった。中身は空っぽでも、Gridのカラムを1つ占有していてくれればいい。これで grid-template-columns のトランジションが成立する。
DOMを「消す」のとCSSで「0pxにする」のは違う。アニメーションさせたいなら、要素は残す。
パネルとチャットの連携は、チャットがパネルを「開く」側、パネルがチャットに「完了報告する」側、という非対称な設計に落ち着いた。双方向というより、イベントの向きが役割で決まってる感じ。
で、全部通ってやっと気持ちよく動き出した。
Flexbox時代は「パネルを開く」操作そのものが、一瞬の不快感を伴ってた。ガクッと動く画面。一瞬だけ見える隙間。操作するたびに小さいストレスが積み重なってた。Gridに切り替えたあとは、意味もなくパネルを何度も開け閉めするようになった。動きそのものが気持ちいいから。UIってこういうところで人を繋ぎとめるんだな、とちょっと思った。
仕組みとしては grid-template-columns にtransitionを効かせるだけの、ほぼ1行の話。でもそこに辿り着くまでに、Flexboxの限界を認めて、レイアウトのパラダイムをひとつ乗り換える必要があった。
アニメーションの滑らかさって、イージング関数とか時間調整の話だと思ってた。でも実際は、そもそもどのプロパティをアニメーションさせるかで勝負が決まる。width でも flex でもなく、grid-template-columns を動かす。聞いて納得した。
Flexboxは要素を並べることには向いてるけど、レイアウト全体の比率を滑らかに変えることには向いてない。Gridはその逆。パラダイムをひとつ乗り換えて、やっと1行にたどり着いた。