画像の最大幅を calc() で制御する

画像の幅をコンテンツに合わせてフレキシブルにしたい場合、親要素の幅を超えないようにするには CSS の max-width プロパティに 100% を指定する。

img {
    max-width: 100%;
    border: 1px solid silver;
    padding: 0.25em;
}

ただ、画像にパディングやボーダーを持たせた場合、その分が親要素の幅より大きくなってしまうことがある (Fig 1)。

Fig 1: パディングとボーダーを持つ画像に `max-width: 100%;` を指定すると、親要素の幅を越えてしまうことがある

これをつねに親要素に収めるようにするには、box-sizing プロパティを利用して画像の幅がパディングとボーダー込みで算出されるようにする、という手がある。

img {
    max-width: 100%;
    border: 1px solid silver;
    padding: 0.25em;
    box-sizing: border-box;
}

これでも上手くいんくんだけど、img 要素の widthheight 属性もこの box-sizing の影響を受けるため、マークアップで指定した値と実際に表示される大きさが違ってしまってちょっと面倒。たとえば、いわゆる Retina ディスプレイを考慮して縦横が 2 倍の画像を width/height で半分の大きさに縮小する、みたいな場面で計算が狂ってくる。

そこで、画像の max-width の値として calc() を利用してみる。

img {
    max-width: calc(100% - 1px * 2 - 0.25em * 2);
    border: 1px solid silver;
    padding: 0.25em;
}

親要素の幅から画像の左右のパディングとボーダーの大きさを引き算したものが画像の最大幅になる。このように % と em と px という異なる単位を一緒にしてもちゃんと計算してくれる。これならマークアップで widthheight を指定しても、親要素の幅が充分にあればそのとおりの大きさで表示され、足りなければパディングとボーダーの分も含めて親要素の幅を超えることはない。

calc() は IE9 を含めすでに けっこうサポートされてる けど、フォールバックとしてはレイアウトが破綻しない程度にざっくり計算した値も入れておく。ベンダー接頭辞として -moz- はたぶんもう要らないけど -webkit- はまだ要る。あと Android と Opera、よろしくお願いします。

img {
    max-width: 96%;
    max-width: -webkit-calc(100% - 1px * 2 - 0.25em * 2);
    max-width: -moz-calc(100% - 1px * 2 - 0.25em * 2);
    max-width: calc(100% - 1px * 2 - 0.25em * 2);
    border: 1px solid silver;
    padding: 0.25em;
}

こういうのは Sass でなんとかなりそうな気がちらっとするけど、Sass はあくまで静的な CSS を書き出すものなので、calc() のようにクライアント側で動的に演算するようなものはそもそも無理。ミックスインを作るとしてもせいぜいベンダー接頭辞を付与するタイプのものしかできないはず。その場合も、単位が混ざってる時点でエラーになってしまうため、式を文字列として渡す必要がある。

@mixin calc ($prop, $expr) {
    $prefixes: webkit, moz;
    @each $prefix in $prefixes {
        #{ $prop }: -#{ $prefix }-calc(#{ $expr });
    }
    #{ $prop }: calc(#{ $expr });
}

img {
    @include calc(max-width, "100% - 1px * 2 - 0.25em * 2");
}

calc() をちゃんと使ったのはじつは今回がはじめてだったんだけど、いままであきらめてたのがあっさり実現できてちょっと感動した。