CSSとJavaScriptで円の中に文字を収める

UI開発者 板垣

複雑なデザインのWebサイトが増えている昨今、皆さんはすべてのデザインを再現できるという自信はありますか?
私はブロークングリッドレイアウトのような複雑なレイアウト以外であれば、とくに調べることなく再現できると自負しておりました。ですが、つい数週間前まであった新入社員研修で新入社員に下記のレイアウトはどうやって再現するのかと聞かれた際、言葉に詰まってしまいました。

その時はテキストを適度に改行させるくらいしか思いつかなかったのですが、その後、調べてみた結果「shape-outside」というプロパティを使用することによって、このレイアウトを再現できることがわかりました。

そもそもshape-outsideプロパティというのはフロートさせた要素の範囲を指定するためのプロパティで、たとえば値に円形を指定すれば、下記のように円に沿ってテキストを配置できます。

今回は、このshape-outsideプロパティのpolygon(x y, x y, ...)関数を用いて、円の中に文字を収めるデザインを実装します。
Polygon以外にもさまざまな関数が使えるので、MDNを参照ください。

それでは、実装例をご覧ください。

HTML

<div class="box">
  <div class="left"></div>
  <div class="right"></div>
  <p>あめんぼあかいなあいうえお</p>
</div>

CSS

.box {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  overflow: hidden;
  border: 2px solid #9d9d9d;
  box-sizing: border-box;
}

.left,
.right {
  height: 100px;
  width: 33.33px;
}

.left {
  float: left;
  shape-outside: polygon(0% 0%, 0% 100%, 100% 100%, 0% 70%, 0% 30%, 100% 0%);
}

.right {
  float: right;
  shape-outside: polygon(100% 0%, 100% 100%, 0% 100%, 100% 70%, 100% 30%, 0% 0%);
}

上記例では.left.rightという空の要素に対して、polygon()関数を使い、円の縁に沿うような多角形を回り込み範囲として指定しています。そうすることで、テキストがその範囲に達したとき改行されます。
HTML上の空の要素を疑似要素で代替したい所ですが、before疑似要素とafter疑似要素の生成される位置関係の問題で、今回は空の要素を使用しています。

基本的にはこの書き方で円の中に文字を収めることができますが、複数箇所に適用しようとすると毎回空の要素を配置しなくてはいけないためコードが冗長になってしまいます。そこで、JavaScriptを使い、コードをよりシンプルにしてみましょう。

HTML

<div class="box shape-inside">
  <p>あめんぼあかいなあいうえお</p>
</div>

JavaScript

const shapeElm = document.querySelectorAll('.shape-inside');
const createShapeInside = (parent) => {
    const fragment = document.createDocumentFragment();
    const create = (className) => {
        const shape = document.createElement('div');
        const {offsetWidth, offsetHeight} = parent;

        shape.style.height = `${offsetHeight}px`;
        shape.style.width = `${offsetWidth / 3}px`;
        shape.classList.add(className);

        return shape;
    };

    fragment.appendChild(create('left'));
    fragment.appendChild(create('right'));

    return fragment;
};

shapeElm.forEach((elm) => {
    elm.prepend(createShapeInside(elm));
});

上記例では.shape-insideクラスがついた要素に対して動的に空要素を追加し、幅と高さを設定することで先ほどと同じレイアウトを実現しています。
shape-outsideを使えば、他にも柔軟なレイアウトが作成できます!ぜひお試しください。