俺と俺のカップルチャンネル

ブログのひとこと説明を入力します。

CSSでつくる透過吹き出し

CSSというより根性で作った。

bonjiri
BONJIRI: an example of CSS speech bubble with transparent background & rounded corners.

作成手順

クラスポン付けで<span class="comment"></span>吹き出しにすることを目指していきます。

背景をつくる

まずこれを作ります。

f:id:nosei:20210311193947p:plain

角が丸くなってるのが要素本体。その下についてる帯はlinear-gradientの透過ピクセルborder-imageにして付けました。角の丸め方は以下の要領です。

f:id:nosei:20210314200325p:plainf:id:nosei:20210311193956p:plain

パディングエリア(緑)の四つ角が同じになるようにボーダー(黒)を曲げているのが分かるでしょうか。コードとしては例えば

.comment {
  border-radius: .4em;
  border-bottom-width: .6em;
  border-bottom-left-radius: .4em calc(.4em + .6em);
  border-bottom-right-radius: .4em calc(.4em + .6em);
}

のように楕円の縦の半径にボーダーの幅を足すといい感じにニュッと曲がります。

ちなみに黒のボーダーはborder-imageに置き換わるので色はどうでもいいですが見えないだけで場所は取ります。

::beforeで枠をのせる

f:id:nosei:20210311194000p:plainf:id:nosei:20210311194004p:plain

::beforeのボーダーで枠を作ります。真ん中下のところはclip-pathで切っておきます。clip-pathはパスで囲ったところだけ表示させるプロパティで、ここでは全体をぐるりと囲いつつ消したいところだけ外へ出すような感じです。

::afterでつのを成型する

f:id:nosei:20210311194008p:plainf:id:nosei:20210311194014p:plain

海苔状の物体は::afterのボーダーです。これをV字に切り抜いて吹き出しのつのにします。

カットして完成

f:id:nosei:20210311195148p:plainf:id:nosei:20210311195154p:plain

.commentのclip-pathではみ出している部分を切ったら完成です。

CSSの実際

polygonのストロークをのけると正味30行くらいですからやってることはわりと単純ですがしかし簡単に見えて意外とこのやり方でないとという部分がありますのでいくらか自注をつけたいと思います。

デモ

要素境界を整える

.comment {
  ...
  background-clip: padding-box;
  border-bottom: solid 1em;
  border-image: linear-gradient(var(--bg-color) 0 1%) 1;
  -webkit-clip-path: polygon(...);
  clip-path: padding-box polygon(...);
}

background-clipは背景の範囲を決めるプロパティで初期値はborder-boxです。背景とボーダーはダブるとブレンドされて色が変わってしまうのでpadding-boxを指定して分離します。

困るのがスケーリングが掛かって要素サイズの計算値に1ピクセル未満の端数が出る場合で、そうなるとborder-imageとパディングエリアはぴったりくっつきません。これは仕様によるもの・バグによるものとUAの解釈によるものがあり、微妙に隙間が空いたり重なったりして本当に往生するわけですが「border-widthborder-image-widthが1:1でborder-image-outsetがゼロ」ならかなりフィットします。つまり何も指定しない時が一番まともです。

border-imageのwidthとoutsetを使う場合単位を付けずに比率で指定し、かつそれぞれ整数倍にしておけば目立つギャップは回避できるようです。

clip-pathの座標計算でも同種の問題にあたりまして単純にボックスの境界でクリップすると削げることがありました。今回の作例ではFFは参照ボックスをpadding-boxにすることでボーダーを食いにくくなりました。webkitは参照ボックスの指定に対応していないのですがもともとの作りとして隙間は空いても削ることはないようです。

続いて::afterでつのの縁取りを作るところです。

.comment::after {
  box-sizing: content-box;
  height: calc(100% - var(--border-w));
  border-bottom: solid .7em var(--border-color);
  clip-path: polygon(...);
}

つのを枠の内側のラインに合わせるために、枠線一本分低いボックスを作って基準にしています。最初はpositionで位置を合わせようとしたんですが例によって精度が出なかったのでまだマシなheightを使っています。box-sizingはどちらでもよいですがborder-boxにしたい場合、箱の外にボーダーを生やすためにborder-image: linear-gradient(var(--border-color) 0 1%) 1 / 1 / 1;としてアウトセットする必要があるかと思います。

その他の検討事項

最初の思いつきとしてはSVGborder-imageで楽々と透過吹き出しが作れるのではと思ったんですがそんなことは全くなかった。そこでボーダーと疑似要素をclip-pathで加工する手法を試みましてある程度の成果があったのではないかと思います。実用上の課題としてはpolygonによる切り抜きが面倒なことだと思います。機械的にやれるとよいのですが。それからつのをカーリーにしたい場合ですね。できそうな気はしますが労力を天秤にかけるとそもそも背景画像として用意するかjavascript吹き出しSVGを生成するほうが沼が浅い予感がします。

今回調べてみてサブピクセル絡みのレンダリングギャップは一般化された対処法もなくかなり厄介だなと感じました。私はもう面倒になってしまったのでスケールを基準に考えればむしろ割り切れるピクセル値が異常であり表示位置はズレるほうが逆に自然なのではと思い始めている次第です。

clip-pathpath()が使えるのでドローソフトの定義がそのままコピペできる…ことになっているけれども現状では座標がボーダーエリアの左上を原点としたピクセル値で、引数に文字列しか受け付けないためcalc()も挟み込めないということで実際には適用できるケースのほうが稀ではないかなと思います。Inline SVG<clipPath>はややサポートが厚いですが、とは言えCSS側の計算値を反映させる工夫が必要になると簡単ではないと思います。clipやmask用途での使い勝手は数年来あまり変わっていないようでありまして先進的なサイトの素敵なサンプルに化かされて手を出すと肥溜めに肩まで浸かることになるので注意が必要だなと思いました。

やっちまったな!