JavaScriptの識別子に中黒が使えるようになった2024年06月07日 10時21分

JavaScriptの識別子(変数名、関数名、プロパティ名など)の2文字目以降に中黒「・」(U+30FB KATAKANA MIDDLE DOT)が使えるようになりました。以下のコードはChrome 124では構文エラーになりますが、Chrome 125では問題なく実行できます。

const シン・ゴジラ = 2016;

JavaScriptの識別子

中黒が使えるようになったのは、JavaScript(ECMAScript)の仕様が変わったからではありません。変わったのはUnicodeの仕様のほうです。Unicode 15.1.0(2023年9月)においてOther_ID_Continueプロパティ(を持つ文字の集まり)に中黒が追加されました。

そもそもJavaScriptの識別子に使える文字は、Unicodeを参照して定義されています。ECMAScript 2023(2023年6月)では以下のようになっています。

識別子の1文字目に使える文字
  • UnicodeのID_Startプロパティを持つ文字
  • $(U+0024 DOLLAR SIGN)
  • _(U+005F LOW LINE)
識別子の2文字目以降に使える文字
  • UnicodeのID_Continueプロパティを持つ文字
  • $(U+0024 DOLLAR SIGN)
  • ゼロ幅非接合子(U+200C ZERO WIDTH NON-JOINER)
  • ゼロ幅接合子(U+200D ZERO WIDTH JOINER)

(いわゆるアンダースコア「_」はID_Continueプロパティを持つため、2文字目以降にも使えます。)

Unicodeの仕様も毎年のように改定されますが、ECMAScript仕様ではUnicodeの「最新バージョン」が参照されています。

Unicodeの識別子

UnicodeのID_StartID_Continueプロパティは、各種の「識別子」に使える文字として推奨されるものを表しており、UAX #31 Unicode Identifiers and Syntaxで以下の文字を含むものとして定義されています。

ID_Startプロパティ
  • 一般カテゴリがLetterである文字
  • 一般カテゴリがLetter_Numberである文字
  • Other_ID_Startプロパティを持つ文字
  • ただしPattern_SyntaxプロパティまたはPattern_White_Spaceプロパティを持つ文字を除く(具体的には(U+2E2F VERTICAL TILDE)が除かれる)
ID_Continueプロパティ
  • ID_Startプロパティを持つ文字
  • 一般カテゴリがNonspacing_Markである文字
  • 一般カテゴリがSpacing_Markである文字
  • 一般カテゴリがDecimal_Numberである文字
  • 一般カテゴリがConnector_Punctuationである文字
  • Other_ID_Continueプロパティを持つ文字
  • ただしPattern_SyntaxプロパティまたはPattern_White_Spaceプロパティを持つ文字を除く

ここでOther_ID_StartプロパティとOther_ID_Continueプロパティというのは、後方互換性のためにそれぞれID_StartプロパティとID_Continueプロパティに含めるべき文字を表しています。Other_ID_Continueプロパティに中黒(U+30FB)が追加されたことで、巡り巡ってJavaScriptの識別子に中黒が使えるようになったのです。

なお、中黒だけでなくゼロ幅非接合子(U+200C)とゼロ幅接合子(U+200D)も追加されたため、ECMAScript 2024以降では識別子の2文字目以降に使える文字の定義が「UnicodeのID_Continueプロパティを持つ文字または$(U+0024 DOLLAR SIGN)」と簡潔になる予定です。

過去にも識別子に中黒が使えた

ID_Continueプロパティの後方互換性のために中黒が追加されたということは、さらに以前は中黒がID_Continueプロパティに含まれていたのでしょうか?

まさにその通りで、Unicode 4.0.1(2004年5月)以前は中黒の一般カテゴリがConnector_Punctuationになっており、結果としてID_Continueプロパティに含まれていました。Unicode 4.1.0(2005年3月)で中黒の一般カテゴリがOther_Punctuationに変更され、ID_Continueプロパティに含まれなくなっていたのです。

ECMAScript 5.1ではUnicode 3.0以上への適合が求められていたため、ECMAScript 3(1999年12月)~5.1(2011年6月)の間は中黒を識別子に使えた可能性があります。(ECMAScript 2(1998年8月)以前はASCIIの範囲内の文字のみ識別子に使用可能、ECMAScript 2015(ES6、2015年6月)以降はUnicode 5.1.0以上への適合が求められる。)

他のプログラミング言語の識別子

識別子の定義にUAX #31を参照しているのはJavaScriptだけではありません。CC++PerlPythonRustなど多くのプログラミング言語がUAX #31を参照しています。これらの言語でもそのうち識別子に中黒が使えるようになるでしょう(\p{Word}との共通部分を採用しているPerlを除く)。

なお、JavaScript以外ではID_StartID_Continueプロパティではなく、XID_StartXID_Continueプロパティを参照していることが多いです。これらのプロパティは、ある識別子にUnicode正規化を適用しても識別子として有効であり続けることを保証するため、それぞれID_StartID_Continueプロパティからいくつかの文字を除外しています。

参考文献

CSSでチェックボックスやラジオボタンをカスタマイズする 2024年版2024年05月24日 09時05分

HTMLのチェックボックス(<input type="checkbox">)やラジオボタン(<input type="radio">)をCSSで装飾したいというのはよく聞く話です。2024年現在は、HTMLの記述は簡単なまま、CSSで自由度の高い装飾も実現できるようになっています。

  1. 結論
  2. 従来の手法
  3. appearanceプロパティを使う手法
  4. 外枠の配置
  5. 未チェックとチェック済みの切り替え
  6. 強制カラーモードへの対応
    1. 透明なボーダーやアウトライン
    2. 内向きの影や背景グラデーション
    3. 画像やテキスト
    4. ブラウザ組み込みの外観
  7. 状態に応じたスタイルの指定
  8. 参考文献

結論

単に色調を整えられればよいという場合は、accent-colorプロパティを使います。

input[type="checkbox"], input[type="radio"] {
  accent-color: #d31;
}

accent-colorプロパティを用いてチェックボックスとラジオボタンを装飾した図

Windows 11 Firefox Nightly 128.0a1での表示

accent-colorプロパティは継承するので、body要素などに指定してその子孫のフォームコントロールに一律に適用することもできます。

自由に装飾したい場合は、input要素にappearance: noneを指定したうえで、未チェック状態とチェック済み状態のスタイルを指定していきます。::before::after疑似要素も使えます。

input[type="checkbox"] {
  -webkit-appearance: none; /* Safari 15.3以下のため */
  appearance: none;
  border: thin solid;
}
input[type="checkbox"]::before {
  content: '✔';
  visibility: hidden;
}
input[type="checkbox"]:checked::before {
  visibility: visible;
}

appearanceプロパティを用いてチェックボックスを装飾した図

Windows 11 Firefox Nightly 128.0a1での表示

従来の手法

これまでチェックボックスやラジオボタンをCSSで装飾したい場合は、input要素自身ではなくその直後に置いたspan要素やlabel要素を装飾するのが一般的でした。

<label>
  <input type="checkbox">
  <span>...</span>
</label>

<input id="the-radio-1" type="radio">
<label for="the-radio-1">...</label>
/* 組み込みのチェックボックスとラジオボタンを見えなくする */
input[type="checkbox"], input[type="radio"] {
  position: absolute;
  clip: rect(0, 0, 0, 0);
}

input[type="checkbox"] + span { /* 未チェック状態のスタイル */ }
input[type="checkbox"]:checked + span { /* チェック済み状態のスタイル */ }
input[type="radio"] + label { /* 未チェック状態のスタイル */ }
input[type="radio"]:checked + label { /* チェック済み状態のスタイル */ }

この手法は、スタイル指定のためだけのspan要素が必要となる、あるいはinput要素にid属性を指定して明示的にlabel要素と紐づける必要があるという点で、ひと手間かかります。また、アクセシビリティを確保するため、input要素を見えなくするのにはdisplay: none以外の手段を使う必要があります(display: noneを使うと、チェックボックスやラジオボタンをキーボードで操作できなくなります)。

appearanceプロパティを使う手法

appearanceプロパティを使う場合、HTML側ではlabel要素とinput要素の暗黙的な紐づけを使えます(明示的な紐づけを使っても構いません)。

<label>
  <input type="checkbox">
  ...
</label>

CSS側ではappearance: noneを指定します。これにより、input要素が組み込みのチェックボックスやラジオボタンではなく、単なる「内容が空の要素」として描画されることになります。<span></span>と同じ扱いができるので、背景、ボーダー、影、アニメーション、それに::before::after疑似要素といったCSSの各種機能をふんだんに使って装飾できます。

外枠の配置

appearance: noneを指定しただけでは内容が空なので何も表示されません。まずは幅と高さを指定してチェックボックスまたはラジオボタンの領域を確保しましょう。幅と高さの指定が確実に効くようにするため、displayプロパティにinline-blockinline-gridinline-flexなどの値を指定する必要があります。

input[type="checkbox"] {
  -webkit-appearance: none; /* Safari 15.3以下のため */
  appearance: none;
  display: inline-block;
  width: 1.4em;
  height: 1.4em;
}

そのままだとブラウザ組み込み(ユーザーエージェントスタイルシート)のスタイルが残ることがあるので、適宜初期化していきます。ブラウザごとの差異を吸収するため、マージンと背景色は常に指定したほうがよいでしょう。em単位を使うのならfont-sizeプロパティを、親要素と同じフォントや文字色(あるいはcurrentcolorキーワード)を使うのならfont-familyプロパティやcolorプロパティを、それぞれ継承させます。

input[type="checkbox"] {
  ...
  margin: 0;
  background-color: transparent;
  color: inherit;
  font: inherit;
}

また、気をつけておきたいのが行内での縦位置です。個人的には、チェックボックスおよびラジオボタンの縦中央と、ラベル文字列の縦中央とがそろったほうが収まりがよく感じられます(vertical-align: middleは英小文字の縦中央とそろえるので、日本語文字の縦中央とは少しずれますが……)。

input[type="checkbox"] {
  ...
  vertical-align: middle;
}

外枠の仕上げとしてボーダーを指定します。ラジオボタンの場合はborder-radiusプロパティで円形にするのがわかりやすいでしょう。ボーダーではなく背景画像やグラデーションで外枠を表現することもできます。後述する強制カラーモードへの対応のため、背景画像を使わないときはボーダーを指定することをお勧めします(border: thin solid transparentといった透明のボーダーでも構いません)。

input[type="checkbox"], input[type="radio"] {
  ...
  border: thin solid;
}
input[type="checkbox"], input[type="radio"] {
  ...
  border-radius: 0.2em;
}
input[type="radio"] {
  ...
  border-radius: 50%;
}

ここまでの指定を適用したのが以下の例です。

未チェックとチェック済みの切り替え

外枠ができたら次は内側、チェック状態の切り替えを表現していきましょう。::before疑似要素でチェック済みを表す内容を指定しつつ最初は隠しておき、チェック済み状態(:checked疑似クラス)になったらその内容を表示するというパターンが多いです。

以下の例では、ボーダーを使ってチェックボックスのチェックマークとラジオボタンの点を描画しています。ボーダーを使っているのは、後述する強制カラーモードでもチェックマークや点が表示されるようにするためです。また、内容を上下左右とも中央ぞろえするためにグリッドレイアウトを使っています。

input[type="checkbox"],
input[type="radio"] {
  ...
  display: inline-grid;
  place-content: center;
}
input[type="checkbox"]::before {
  content: '';
  width: 0.7em;
  height: 0.3em;
  border-left: 0.3em solid;
  border-bottom: 0.2em solid;
  transform: translateY(-0.1em) rotate(-40deg);
  visibility: hidden;
}
input[type="checkbox"]:checked::before {
  visibility: visible;
}
input[type="radio"]::before {
  content: '';
  border: 0.4em solid;
  border-radius: 50%;
  visibility: hidden;
}
input[type="radio"]:checked::before {
  visibility: visible;
}

clip-pathプロパティを使って好きな形に切り抜いたり、背景画像やボーダー画像にグラデーションを使ったりと、自由度の高い表現が可能です。

input[type="checkbox"]::before {
  content: '';
  border: 0.5em solid;
  clip-path: polygon(0% 60%, 0% 45%, 35% 65%, 100% 0%, 100% 15%, 45% 100%, 30% 100%);
  visibility: hidden;
}
input[type="radio"]::before {
  content: '';
  width: 1em;
  height: 1em;
  border: 0.2em solid transparent;
  border-radius: 50%;
  padding: 0.2em;
  background:
    repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) content-box,
    repeating-conic-gradient(from 45deg, #fc6 0deg 90deg, #f33 90deg 180deg) padding-box,
    repeating-conic-gradient(from 45deg, #f33 0deg 90deg, #fc6 90deg 180deg) border-box,
    CanvasText;
  visibility: hidden;
}

チェック状態の切り替え時に、フェードイン・フェードアウトや拡大・縮小といったアニメーション効果をつけるのもよいでしょう。

input[type="checkbox"]::before {
  ...
  opacity: 0;
  transition: opacity 0.5s ease-out;
}
input[type="checkbox"]:checked::before {
  opacity: 1;
}

input[type="radio"]::before {
  ...
  scale: 0;
  transition: scale 0.5s ease-out;
}
input[type="radio"]:checked::before {
  ...
  scale: 1;
}

強制カラーモードへの対応

強制カラーモードとは、テキストを読みやすくするために、背景色や文字色をコントラストのはっきりした色にする機能のことです。OSのアクセシビリティ機能の一環として提供されることが多く、Windowsなら「ハイコントラスト(Windows 10)」「コントラストテーマ(Windows 11)」という設定を有効にすることで、ブラウザで表示中の文書でも強制カラーモードが有効化されます。

Windows 11のコントラストテーマ「夕暮れ」を適用した図

強制カラーモードの主な影響は以下の通りです。詳しくはforced-colorsメディア特性を参照してください。

  • colorbackground-colorborder-coloroutline-coloraccent-colorなどは、OSやブラウザ側で決められた色に変更されます。
  • text-shadowbox-shadowは無視されます。
  • background-image: linear-gradient(...)などのグラデーションの指定は無視されます。

一般的なテキストと画像に関しては、強制カラーモードを意識せずにスタイルを指定していても、最低限テキストが読める状態になることが多いです。しかし、カスタマイズしたチェックボックスやラジオボタンに関しては、気をつけないと全く表示されず、ユーザーがそれらフォームコントールの存在に気づかないという事態が起こりかねません。

チェック状態に関しても、チェックマークや点を単純にbackground-color: #d51のような背景色だけで表現してしまうと、強制カラーモードではチェック済みかどうか視認できなくなってしまいます。前述のボーダーを使った表現や、または以下に挙げるような表現手法をとる必要があります。

透明なボーダーやアウトライン

背景部分に透明なボーダーやアウトラインを重ねる手法です。強制カラーモードが無効なときは背景がそのまま表示されますし、有効なときはボーダーやアウトラインが塗りつぶされて視認できます。背景部分にアウトラインを重ねるためには、outline-offsetプロパティの値に負数を指定してボックスの内側にアウトラインを配置します。

input[type="checkbox"]::before {
  content: '';
  border: 0.4em solid transparent;
  background-color: #d31;
  visibility: hidden;
}
input[type="radio"]::before {
  content: '';
  width: 0.8em;
  height: 0.8em;
  border-radius: 50%;
  background-color: #d31;
  outline: 0.4em solid transparent;
  outline-offset: -0.4em;
  visibility: hidden;
}

内向きの影や背景グラデーション

CanvasText(文書の文字色を示す)といったシステム色は、強制カラーモードでも視認できる背景色として使えます。内向きの影(box-shadow: inset ...)は背景の前面に描画されるため、強制カラーモードが無効なら影の色が表示されますが、有効だと背景色が表示されることになります。背景画像としてグラデーションを使った場合も、そのグラデーションは背景色の前面に描画されます。

input[type="checkbox"]::before {
  content: '';
  width: 0.8em;
  height: 0.8em;
  background-color: CanvasText;
  box-shadow: inset 0 0 0 0.4em #d31;
  visibility: hidden;
}
input[type="radio"]::before {
  content: '';
  width: 0.8em;
  height: 0.8em;
  border-radius: 50%;
  background: linear-gradient(#d31 0% 100%), CanvasText;
  visibility: hidden;
}

画像やテキスト

background-image: url(...)content: url(...)で読み込んだ画像、およびcontentプロパティで指定したテキストは、強制カラーモードでもそのまま表示されます。

input[type="checkbox"]::before {
  content: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2010%2010%22%3E%3Cpath%20d%3D%22M%205%2C1%20L%209%2C5%205%2C9%201%2C5%20Z%22%20fill%3D%22%23fda%22%20stroke%3D%22%23f81%22%2F%3E%3C%2Fsvg%3E");
  width: 1.2em;
  height: 1.2em;
  visibility: hidden;
}
input[type="radio"] {
  ...
  display: inline-block;
  text-align: center;
}
input[type="radio"]::before {
  content: '';
  display: block;
  margin-top: -0.2em;
}
input[type="radio"]:checked::before {
  content: '👉';
}

ブラウザ組み込みの外観

強制カラーモードではカスタマイズせずに、ブラウザ組み込みの外観を使うのもひとつの手です。forced-colorsメディア特性で強制カラーモードが有効かどうかを判別でき、allプロパティrevertキーワードの組み合わせでスタイル指定を撤回できます。

@media (forced-colors: active) {
  input[type="checkbox"],
  input[type="checkbox"]::before,
  input[type="radio"],
  input[type="radio"]::before {
    /* 疑似クラスに指定されたスタイル(後述)も撤回したいので!importを指定する。 */
    all: revert !important;
  }
}

ここまで書いておいてなんですが、筆者は強制カラーモードを常用しておらず、強制カラーモードでチェックボックスやラジオボタンをカスタマイズしてもよいのかしないほうがよいのかのベストプラクティスを持ち合わせていません。ご存じの方は教えていただけると幸いです。

状態に応じたスタイルの指定

以上で最低限の表示切替ができましたが、ほかにも状態に応じたスタイルを指定できます。

よく使うのはマウスホバー時を表す:hover疑似クラスでしょう。フォームコントロールに対する:hover疑似クラスは、そのフォームコントロールに紐づけられたlabel要素にマウスホバーしているときにも適用されます。

次の例では:hover疑似クラスと:enabled疑似クラスを組み合わせています。:hover疑似クラスを使うのは「今まさにユーザーが操作できる対象を強調したい」という意図であり、無効状態の(操作できない)フォームコントロールに適用してしまうとその意図から外れてしまうからです。

input[type="checkbox"]:enabled:hover,
input[type="radio"]:enabled:hover {
  box-shadow: 0 0 0.3em 0.1em #d31;
}

多くのブラウザでは、チェックボックスやラジオボタンにキーボード(Tabキー)でフォーカスしたときに、フォーカスリングが表示されます。このフォーカスリングの表示を変更したいときには:focus-visible疑似クラスを使います。その際には「アクセシビリティの考慮」の項WCAG 2.1の達成基準1.4.11「非テキストのコントラスト」の解説も参考に、「今まさにユーザーが操作できる対象」が伝わるようにしてください。

以下の例では、影を使ってフォーカスリングを表現しつつ、強制カラーモードに備えて透明のアウトラインを設定しています。単にoutline-color: transparentを指定するだけ(outline-style: autoのまま)だとブラウザによってはアウトラインが見えてしまうことがあるので、outline-style: solidにしています。

input[type="checkbox"]:focus-visible,
input[type="radio"]:focus-visible {
  box-shadow: 0 0 0.3em 0.1em #d31;
  outline: medium solid transparent;
}

無効状態の(disabled属性が付与された)チェックボックスやラジオボタンを提供するのなら、:disabled疑似クラスを使います。次の例ではfilterプロパティを使って無彩色にしたうえで、opacityプロパティで色を薄くしています。強制カラーモードで背景が明色になろうと暗色になろうと、半透明なら色が薄くなることに変わりありません。

input[type="checkbox"]:disabled,
input[type="radio"]:disabled {
  border-style: dashed;
  filter: grayscale(1);
  opacity: 0.6;
}

チェックボックスには未チェックともチェック済みとも言えない未決定状態が存在します。ただし、未決定状態になるのはJavaScriptでinput要素オブジェクトのindeterminateプロパティを設定したときだけです。そのようなJavaScriptの記述がなければ、未決定状態に対するスタイルを指定する必要はありません。そのような記述があるのなら、:indeterminate疑似クラスを使ってスタイルを指定します。

以下の例では「くだもの」グループに属すチェックボックスの一部のみがチェックされているときに、「くだもの」チェックボックスを未決定状態にしています。

input[type="checkbox"]:indeterminate::before {
  width: 0.5em;
  height: 0.5em;
  border: 0.2em dotted;
  transform: none;
  visibility: visible;
}

参考文献

チェックボックスとラジボタンのカスタマイズについて:

強制カラーモードについて:

Perlのレキシカルサブルーチンとperlcritic2022年12月21日 02時48分

この記事はPerl Advent Calendar 2022の21日目の分です。


Perlでは、関数内で定義した関数も外部から見えてしまいます。

use feature 'say';

sub foo {
    sub bar {
        say 'bar';
    }
    bar();
}

# foo関数の外でもbar関数を呼び出せる。
bar();

特定のスコープでのみ参照できる関数を定義したいときは、関数定義をsubではなくmy sub(またはstate sub)から始めます。この機能はレキシカルサブルーチン(lexical subroutinesと呼ばれます。

use feature 'say';

sub foo {
    my sub bar {
        say 'bar';
    }
    bar();
}

# 未定義の関数呼び出しによる例外が発生する。
bar();

ちょっとした処理をまとめるのに便利なレキシカルサブルーチンですが、perlcriticとの組み合わせに難がありました。レキシカルサブルーチンを使ったコードをperlcriticにかけると、Subroutines::ProhibitNestedSubsポリシーSubroutines::ProhibitBuiltinHomonymsポリシーのエラーが出てしまうのです。(perlcriticはPerl向けのリンターです。詳しくは「perlcriticとのつきあい方 - 私が歌川です」などを参照してください。)

Subroutines::ProhibitNestedSubsポリシーは関数の入れ子を禁止します。入れ子の内側の関数が意図せず外部に公開されるのを避けるためのものなので、もともと外部に公開されないレキシカルサブルーチンに対しては禁止する意味がありません。

Subroutines::ProhibitBuiltinHomonymsポリシーは組み込み関数と同名の関数を禁止します。perlcriticの内部で使われるで使われるPPIモジュールの不具合により、レキシカルサブルーチンの名前は常にsubであるとみなされていました。

これらの問題を解決するため、昨年perlcriticPPIに以下のプルリクエストを提出しました。

この年末までにこれらの変更がすべて取り込まれたので、perlcritic(とPPI)のバージョンを最新にすれば、心置きなくレキシカルサブルーチンを使えます。

CSSのconic-gradientで直線的な模様を作る2022年12月19日 11時11分

この記事はCSS Advent Calendar 2022の19日目の分です。


CSSのconic-gradient(扇形グラデーション)関数を使うと、円グラフや集中線のような表現ができます。ここであえて扇形の中心角の部分に注目し、直線的な模様を作ってみることはできないでしょうか? いくつか試してみました。

例1. 二重のボーダー

「二重のボーダー」のデモ

「二重のボーダー」のCSSコード
p {
  --outer-border-width: 1em;
  --inner-border-width: 1em;
  --outer-border-top-left-color: #1bf;
  --inner-border-top-left-color: #35b;
  --inner-border-bottom-right-color: #dc6;
  --outer-border-bottom-right-color: #3a8;
  margin: 0;
  padding: 1em;
  border: calc(var(--outer-border-width) + var(--inner-border-width)) solid transparent;
  background:
    conic-gradient(from 180deg at var(--outer-border-width) var(--outer-border-width), var(--outer-border-top-left-color) 270deg, transparent 270deg) no-repeat border-box 0 0 / calc(100% - var(--outer-border-width)) 100%,
    conic-gradient(from 180deg at var(--inner-border-width) var(--inner-border-width), var(--inner-border-top-left-color) 270deg, transparent 270deg) no-repeat border-box var(--outer-border-width) var(--outer-border-width) / calc(100% - 2 * var(--outer-border-width) - var(--inner-border-width)) calc(100% - 2 * var(--outer-border-width)),
    conic-gradient(from 0deg at calc(100% - var(--inner-border-width)) calc(100% - var(--inner-border-width)), var(--inner-border-bottom-right-color) 270deg, transparent 270deg) no-repeat border-box calc(var(--outer-border-width) + var(--inner-border-width)) var(--outer-border-width) / calc(100% - 2 * var(--outer-border-width) - var(--inner-border-width)) calc(100% - 2 * var(--outer-border-width)),
    conic-gradient(from 0deg at calc(100% - var(--outer-border-width)) calc(100% - var(--outer-border-width)), var(--outer-border-bottom-right-color) 270deg, transparent 270deg) no-repeat border-box var(--outer-border-width) 0 / calc(100% - var(--outer-border-width)) 100%;
}

「二重のボーダー」の画像

例2. 矢印

「矢印」のデモ

「矢印」のCSSコード
p {
  --arrow-bg-color: #aaa;
  --arrow-color: #ff5;
  --arrow-angle: 120deg;
  --arrow-from-angle: calc(180deg + var(--arrow-angle) / 2);
  --arrow-outer-angle: calc(360deg - var(--arrow-angle));
  --arrow-margin-left: 1em;
  --arrow-width: 2em;
  --arrow-center: calc(var(--arrow-width) / 2);
  --arrow-offset: 1em;
  margin: 0;
  padding: 1em;
  border: solid transparent;
  border-width: 0 0 0 calc(var(--arrow-margin-left) + var(--arrow-width));
  border-radius: 0.5em;
  background: #ddd no-repeat border-box var(--arrow-margin-left) 0 / var(--arrow-width) 100%;
  background-image:
    conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(1 * var(--arrow-offset)), var(--arrow-bg-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)),
    conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(2 * var(--arrow-offset)), var(--arrow-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)),
    conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(3 * var(--arrow-offset)), var(--arrow-bg-color) var(--arrow-outer-angle), transparent var(--arrow-outer-angle)),
    conic-gradient(from var(--arrow-from-angle) at var(--arrow-center) calc(4 * var(--arrow-offset)), var(--arrow-color) var(--arrow-outer-angle), var(--arrow-bg-color) var(--arrow-outer-angle));
}

「矢印」の画像

例3. 星形

「星形」のデモ

「星形」のCSSコード
p {
  --outer-block-angle: 6deg;
  --outer-inline-angle: 22deg;
  --cone-angle: calc(90deg - var(--outer-block-angle) - var(--outer-inline-angle));
  --cone-top-left-color: #bde;
  --cone-top-right-color: #cae;
  --cone-bottom-right-color: #ecc;
  --cone-bottom-left-color: #ffb;
  margin: 0;
  padding: 2em 3em;
  background:
    conic-gradient(from calc(90deg + var(--outer-block-angle)) at 0 0, var(--cone-top-left-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 0 0 / 50% 50%,
    conic-gradient(from calc(180deg + var(--outer-inline-angle)) at 100% 0, var(--cone-top-right-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 100% 0 / 50% 50%,
    conic-gradient(from calc(270deg + var(--outer-block-angle)) at 100% 100%, var(--cone-bottom-right-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 100% 100% / 50% 50%,
    conic-gradient(from var(--outer-inline-angle) at 0 100%, var(--cone-bottom-left-color) var(--cone-angle), transparent var(--cone-angle)) no-repeat 0 100% / 50% 50%;
}

「星形」の画像

例4. 吹き出し

「吹き出し」のデモ

「吹き出し」のCSSコード
p {
  --bg-color: #fed;
  --padding: 1em;
  --border-width: 0.5em;
  --border-color: #f93;
  --border-radius: 1em;
  --balloon-tail-angle: 60deg;
  --balloon-tail-from-angle: calc(90deg - var(--balloon-tail-angle) / 2);
  --balloon-tail-width: 2em;
  --balloon-tail-height: calc(1.15470054 * var(--balloon-tail-width)); /* 1.15470054 = 2 / sqrt(3) */
  --balloon-tail-bottom-offset: 1em;
  margin: 0 0 0 var(--balloon-tail-width);
  padding: var(--padding);
  border: var(--border-width) solid var(--border-color);
  border-radius: var(--border-radius);
  background: var(--bg-color);
}
p::after {
  content: '';
  position: absolute;
  display: block;
  width: calc(var(--balloon-tail-width) + var(--border-width));
  height: var(--balloon-tail-height);
  background:
    conic-gradient(from var(--balloon-tail-from-angle) at calc(2 * var(--border-width)) calc(var(--balloon-tail-height) / 2), var(--bg-color) var(--balloon-tail-angle), transparent var(--balloon-tail-angle)) no-repeat,
    conic-gradient(from var(--balloon-tail-from-angle) at 0 calc(var(--balloon-tail-height) / 2), var(--border-color) var(--balloon-tail-angle), transparent var(--balloon-tail-angle)) no-repeat;
  transform: translate(calc(0em - var(--padding) - var(--balloon-tail-width) - var(--border-width)), calc(var(--padding) + var(--border-width) - var(--border-radius) - var(--balloon-tail-height) - var(--balloon-tail-bottom-offset)));
}

「吹き出し」の画像

感想

やってはみたもののどうも微妙です。直線的な模様ならだいたいはlinear-gradient関数で表現できますし、要素を切り抜くのならclip-pathプロパティのほうが自由度が高いです。conic-gradient関数を使えば記述量を減らせるかというと、そういうこともそんなになく、やはり目的外の利用はあまりうまくいかないのかもしれません。

今回ひとつ得られた教訓は、背景画像を使って疑似的にボーダーを表現する場合、疑似的なボーダーと同じ大きさの透明なボーダー(border: <border-wdith> solid transparent)を指定したほうがよいということです。そうすることで、overflow: autoで内容がはみ出したときなどにスクロール領域が疑似的なボーダーにかからず、より自然に見えます。

CSSの絶対配置の要素の静的位置矩形2022年12月11日 12時52分

この記事はCSS Advent Calendar 2022の11日目の分です。


CSSでpositoin: absolute(絶対配置)の要素の位置を指定するときには、topleftrightbottomプロパティ(またはこれらを一括指定するinsetプロパティ)がよく使われます。もし絶対配置の要素にそれらのプロパティが指定されていなかったら、その要素の位置はどこになるでしょうか?

その場合、絶対配置の要素は原則として「その要素がpositoin: static(静的配置)だった場合の位置(静的位置矩形; static-position rectangle)」に置かれます。絶対配置の要素の親要素がインライン要素だった場合、絶対配置の要素自身がdisplay: inline(インライン要素)かdisplay: block(ブロック要素)かによって位置が異なってくることになります(インライン要素の子要素が絶対配置のときのデモ)。

この挙動をうまく利用すれば、絶対配置の要素の親要素にposition: relativeをつけて回らなくても、絶対配置の要素を期待する位置に置けることがあります。その場合、細かな位置の調整にinsetプロパティなどを使うことはできないので、transform: translate(...)marginプロパティを使うことになります。

フレックスアイテムが絶対配置のとき

「原則として」というからには例外もあります。絶対配置のフレックスアイテムでinsetプロパティなどが指定されていないものは、フレックスコンテナの位置に置かれます。

(「フレックスコンテナ」はdisplay: flexまたはdisplay: inline-flexが指定された要素、「フレックスアイテム」はフレックスコンテナの子要素です。)

グリッドアイテムが絶対配置のとき

グリッドアイテムが絶対配置のときはちょっと複雑です。グリッドコンテナが静的配置なら、絶対配置のグリッドアイテムでinsetプロパティなどが指定されていないものは、グリッドコンテナの位置に置かれます。

グリッドコンテナが静的配置でないなら、絶対配置のグリッドアイテムでinsetプロパティが指定されていないものは、gridプロパティなどで指定されたグリッド領域の位置に置かれます(グリッドアイテムが絶対配置のときのデモ)。

(「グリッドコンテナ」はdisplay: gridまたはdisplay: inline-gridが指定された要素、「グリッドアイテム」はグリッドコンテナの子要素です。)

とはいえ、フレックスボックスやグリッドと絶対配置を組み合わせるとCSSのコードがだいぶ複雑になるので、普段は避けたほうがよいと思います。