Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

left:50%とwidth:fit-contentを組むとmax-widthが効かない

公開 読了時間 約6分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

position:absoluteでleft:50%指定の要素にwidth:fit-contentを使うと、shrink-to-fitのavailable widthがcontaining block幅からleftオフセット分だけ縮められるため、maxWidthより先に幅の上限へ到達し、maxWidthは一度も効かないまま無視される。

結論

position:absoluteの要素でwidthがauto(shrink-to-fit)、leftが指定済みでrightがautoのとき、shrink-to-fitの計算に使うavailable widthはcontaining blockの幅からleftオフセット分を差し引いた値になる。 left:50%で中央寄せしていると、それだけでavailable widthがcontaining block幅の半分に縮み、maxWidthをいくら大きく指定しても、その値に到達する前にavailable widthの天井へぶつかって折り返される。maxWidthは一度も評価されない。

症状

動画レンダリング(Remotion)のシーン内に、字幕テキストを表示するプレート要素があった。横幅は文字数に応じて可変にしたいのでwidth:fit-contentを使い、画面中央に固定するためleft:50%+transform:translateX(-50%)で中央寄せしていた。

<div
  style={{
    position: 'absolute',
    left: '50%',
    bottom: 80,
    transform: 'translateX(-50%)',
    opacity: enter,
    width: 'fit-content',
    maxWidth: 1600,
    padding: '16px 32px',
    borderRadius: radiusSm,
    backgroundColor: bg,
    textAlign: 'center',
  }}
>
  <span style={{ color, fontSize: 42, fontWeight: 500, lineHeight: 1.5, letterSpacing: '0.02em', wordBreak: 'keep-all' }}>
    {lines[lineIdx]}
  </span>
</div>

containing blockは1920px幅の合成キャンバスで、想定していた最大幅は1600pxだった。1600px以内に収まる長さの文字列でも、実際には途中で強制的に2行へ折り返される行がいくつも発生した。fit-contentが伸びきる前に幅が頭打ちになっている挙動で、maxWidth:1600を1600のまま変えても症状は変わらなかった。

副作用として、2行化した分だけプレートの高さが増え、プレート上端が想定より高い位置までせり上がって、下に重なっている別レイヤーの表示と干渉していた。

原因

CSS 2.1のposition:absolute要素に対する幅算出アルゴリズムでは、width: auto(shrink-to-fit)かつleftが数値指定・rightautoの場合、shrink-to-fitの計算に使う available width は次の式で決まる。

available width = containing block の幅 - left オフセット - margin

このケースでは containing block 幅が1920px、left: '50%' はcontaining block基準で960pxに解決されるため、

available width = 1920 - 960 = 960px

となる。transform: translateX(-50%) は描画後の見た目の位置をずらすだけで、幅算出アルゴリズムが参照する left の値そのものには影響しない。shrink-to-fitのアルゴリズムは、コンテンツの内容幅(preferred width)と available width を比較し、内容幅の方が大きければ available width で頭打ちにする。maxWidth: 1600 はこの後の段階でmin/maxの制約として適用されるはずの値だが、頭打ちが960pxの時点で先に確定してしまうため、1600という指定は一度も意味を持たない。

left の値を50%より小さくすればavailable widthは広がるが、それでは中央寄せができなくなる。left:50%+transform:translateX(-50%)という中央寄せの定石そのものが、shrink-to-fit幅の計算とは相性が悪い。

直し方

absoluteの対象を「中央寄せ」ではなく「containing blockいっぱいに広げるラッパー」に変え、中央寄せ自体はflexboxに任せた。可変幅の本体はflexアイテムとして通常のフローに置くことで、leftオフセットの影響を受けるabsoluteの幅算出から切り離した。

<div style={{ position: 'absolute', left: 0, right: 0, bottom: 80, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
  <div
    style={{
      opacity: enter,
      width: 'max-content',
      maxWidth: 1500,
      padding: '16px 32px',
      borderRadius: radiusSm,
      backgroundColor: bg,
      textAlign: 'center',
    }}
  >
    <span style={{ color, fontSize: 46, fontWeight: 500, lineHeight: 1.5, letterSpacing: '0.02em', wordBreak: 'keep-all' }}>
      {lines[lineIdx]}
    </span>
  </div>
</div>

外側のdivleft: 0; right: 0でcontaining block全幅を占有する。これはwidth: autoだがleftrightが両方指定されているケースなので、先述のshrink-to-fitアルゴリズムは使われず、幅はそのままcontaining block全幅として確定する。中央寄せは内側のjustify-content: centerが担う。

内側のdivpositionを持たない通常フローの要素として、外側のflexコンテナに1個だけ子として並ぶ。この位置ではshrink-to-fit幅の算出にleftオフセットが関与しないため、width: max-contentmaxWidth: 1500が意図どおりに効く。あわせてmaxWidthを1600から1500へ、フォントサイズを42から46へ調整しているが、これは見た目の再調整であって幅バグの直しの本体ではない。

再発防止

修正後のコードには、幅算出の仕組みに触れたコメントを残した。

// 旧 left:'50%'+width:'fit-content' は shrink-to-fit の
// available width が 1920-960=960px に固定され、maxWidth:1600 が一度も効かず全行が2行化
// →プレート上端が想定より高くせり上がり下段レイヤを覆う副作用の根だった。
// フルブリードのflexラッパ+max-contentで実幅に。

position: absoluteleftだけを指定して中央寄せする書き方は見た目上は動くため、幅が固定長のコンテンツでは症状が出ない。可変長のテキストをwidth: fit-contentwidth: max-contentと組み合わせたときにだけ、available widthの縮小が表面化する。中央寄せ目的のabsoluteに可変幅を持たせたくなったら、まずabsoluteをcontaining block全幅に広げてflexで中央寄せする形に倒すのが安全だとわかる。

よくある質問

Q1left:50%とwidth:fit-contentの組み合わせでmax-widthが効かないのはなぜですか?

position:absoluteでwidthがautoかつleftが指定・rightがautoの要素は、shrink-to-fit幅の計算に使うavailable widthをcontaining blockの幅からleftオフセット分を差し引いた値で求めます。left:50%なら残りは半分しかないため、実際に許される幅がmaxWidthよりずっと小さい値に固定され、maxWidthの数値は一度も評価される前に頭打ちになります。

Q2中央寄せしたまま可変幅を維持するにはどうすればいいですか?

absoluteをcontaining blockいっぱいに広げる用途に限定し(left:0; right:0)、中央寄せはflexboxのjustify-content:centerに任せます。可変幅の本体は内側のflexアイテムとして通常フローに置き、そこでwidth:max-contentとmax-widthを指定すれば、leftオフセットに幅を食われることなくmax-widthどおりに頭打ちできます。

Q3この挙動はブラウザのバグですか?

いいえ、CSS 2.1のabspos幅算出アルゴリズムどおりの仕様です。width:autoかつleft指定・right:autoの組み合わせでshrink-to-fitを使う場合の available width の定義がleftオフセットを差し引く形になっているため、どのブラウザでも同じ結果になります。

確認した環境

  • Remotion 4.0.500 / React 19.2.8
  • 2026-07-31 の修正コミットで解消(発生時期はそれ以前の実装当初から)

この記事の根拠

  • TypeScriptファイル 392〜424行目コミット cec0dd8
  • TypeScriptファイル 392〜440行目コミット 57e6a99

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。