CSSで、方眼紙のようなマス目の背景を作る方法

​​CSS​HTML,Playwright

方眼紙のようなマス目の背景は、画像を用意しなくても、CSS のグラデーションだけで描けます。縦線を描くグラデーションと横線を描くグラデーションを重ね、background-size でマスの大きさを決めると、そのマスが縦横に繰り返されます。5マスごとの濃い線、箱の端を線で閉じる方法、マスの大きさに小数が入ると線がにじむ問題、入力欄の文字をマスの行にそろえる方法もあわせて書きます。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、getComputedStyle() の値と、画面を撮って線の色の画素がある位置を記録しました。Firefox と Safari、スマートフォンの実機は試していません。

縦線と横線のグラデーションを2枚重ねる

CSS は次のとおりです。

css
/* 20pxごとに、細い水色の線でマス目を描く */
.grid-paper {
  background-color: #fff;
  background-image:
    linear-gradient(to right, #9fdcea 1px, transparent 1px),
    linear-gradient(to bottom, #9fdcea 1px, transparent 1px);
  background-size: 20px 20px;
}

マス目を出したい要素に、このクラスを付けます。

html
<div class="grid-paper"></div>
白い背景に、水色の細い線で20px四方のマスが縦横に並んだ方眼紙の模様
20px四方のマスが、縦横に繰り返される

1つ目の linear-gradient() は、左から右へ色が変わります。左端の1pxだけを水色にし、同じ1pxの位置から先を透明にしているので、色はぼかされずに切り替わり、幅1pxの縦線になります。2つ目は上から下へ色が変わるので、上端の1pxが横線になります。

background-size で、この2枚を20px四方の大きさにします。background-repeat が初期値の repeat のままなので、背景は縦横に繰り返され、20px四方のマスの左端と上端に線が描かれ、それが並んでマス目になります。

幅400px、高さ200pxの <div> で、画面を撮って調べました。マスの中を通る横の1行では、白でない画素は x が 0、20、40…380 の位置に1pxずつ、20本ありました。色はどれも rgb(159, 220, 234)(#9fdcea)です。縦の1列でも、y が 0、20…180 の位置に1pxずつ、10本の横線がありました。

デバイスピクセル比(Playwright の deviceScaleFactor)を変えても試しました。2にすると、線はデバイスピクセルで2px、間隔は40pxになり、1.5にすると線は1px、間隔は30pxでした。どちらも、線の画素の色は同じ #9fdcea で、にじんでいませんでした。

background-size に値を1つだけ書くと、横線が消える

background-size には、幅と高さの2つの値を書きます。20px のように1つだけ書くと、横線が最初の1本しか描かれませんでした。

上下2段の比較。上の「background-size: 20px 20px」ではマス目が描かれている。下の「background-size: 20px(値が1つ)」では縦線だけが並び、横線は上端の1本だけで、マス目になっていない
値が1つだと、縦線しか並ばない(下)

計算値は 20px auto で、値が1つのときは、高さが auto になっていました。MDN の説明によると、高さが auto で、画像に決まった大きさも縦横の比率もない場合、高さは背景を置く領域の高さになります。グラデーションには、決まった大きさがありません。横線のグラデーションが箱の高さいっぱいに引き伸ばされ、上端に1本だけ描かれたことになります。

5マスごとに濃い線を重ねる

方眼紙のように5マスごとに濃い線を入れるときは、同じ形のグラデーションを2枚足して、background-size を5マス分(100px)にします。

css
/* 5マスごとに濃い線を重ねる(先に書いた層が上に描かれる) */
.grid-paper-major {
  background-color: #fff;
  background-image:
    linear-gradient(to right, #00a3c4 1px, transparent 1px),
    linear-gradient(to bottom, #00a3c4 1px, transparent 1px),
    linear-gradient(to right, #9fdcea 1px, transparent 1px),
    linear-gradient(to bottom, #9fdcea 1px, transparent 1px);
  background-size:
    100px 100px,
    100px 100px,
    20px 20px,
    20px 20px;
}
白い背景に、水色の細い線で20px四方のマスが並び、5マスごとの線だけが濃い水色になっている方眼紙の模様
5マスごとに、濃い線が入る

background-size は、カンマで区切った順に background-image の1枚ずつに対応します。background-image の説明にあるとおり、先に書いた画像ほど手前に描かれます。濃い線を先に書くと、細い線と重なる位置では濃い線が見えます。

試したところ、横の1行の線の色は、x が 0、100、200、300 の位置だけが濃い rgb(0, 163, 196) で、そのほかの 20、40、60、80、120… は rgb(159, 220, 234) のままでした。

箱の右端と下端を線で閉じる

この描き方では、線はマスの左端と上端にしかありません。箱の幅と高さがマスの倍数だと、右端と下端はマスの境目で終わりますが、そこには線がありません。倍数でなければ、端のマスが途中で切れます。

上下2段の比較。上の「箱が 250×110px(マスの倍数ではない)」では、右端と下端のマスが途中で切れていて、端に線がない。下の「箱が 241×121px(マスの倍数 + 1px)」では、右端と下端にも線があり、マスがきちんと閉じている
箱を「マスの倍数 + 1px」にすると、端も線で閉じる(下)

250×110px の箱では、最後の縦線は x が 240、最後の横線は y が 100 の位置で、右端の列(x が 249)は白でした。マス目を表のように端まで線で閉じたいときは、箱の大きさを「マスの倍数 + 1px」にします。

css
/* 右端と下端にも線が来るように、箱の大きさを「マスの倍数 + 1px」にする */
.grid-closed {
  --cell: 20px;
  width: calc(var(--cell) * 12 + 1px);
  height: calc(var(--cell) * 6 + 1px);
  background-image:
    linear-gradient(to right, #9fdcea 1px, transparent 1px),
    linear-gradient(to bottom, #9fdcea 1px, transparent 1px);
  background-size: var(--cell) var(--cell);
}

箱は 241×121px になり、縦線は x が 0 から 240 までの13本、横線は y が 0 から 120 までの7本で、右端と下端にも線が描かれました。

マスの大きさに小数が入ると、線がにじむ

文字の大きさに合わせてマスを rem で決めると、マスの大きさに小数が入ることがあります。background-size: 1.3rem 1.3rem にすると、計算値は 20.8px 20.8px でした。

このとき、線の位置は 0、20〜21、41〜42、62〜63… のように1pxと2pxが入りまじり、色も rgb(159, 220, 234) から rgb(243, 251, 252) まで、線ごとにばらばらでした。1pxの線が2つの画素にまたがって描かれ、薄くにじんだ線になります。

上下2段の比較。マス目の左上を4倍に拡大している。上の「1.3rem(20.8px)のまま」では、線ごとに太さと濃さが違い、2画素にまたがる薄い線が混じっている。下の「round(1.3rem, 1px)(21px)に丸める」では、どの線も1画素の同じ濃さで描かれている
マスの大きさを1px単位に丸めると、線の太さと濃さがそろう(下)

round() で、マスの大きさを1px単位に丸めると直りました。

css
/* rem で決めたマスの大きさを、1px 単位に丸める */
.grid-rounded {
  background-size: round(1.3rem, 1px) round(1.3rem, 1px);
}

計算値は 21px 21px になり、縦線は x が 0、21、42… の位置に1pxずつ、色はどれも rgb(159, 220, 234) でした。

入力欄では、文字をマスの行にそろえ、background-attachment を local にする

<textarea> にマス目を敷いて、ノートのように文字を書けるようにするときは、行の高さ(line-height)をマスの大きさと同じにします。これで1行が1マスの高さになり、上の余白(padding-top)を0にすると、その行がマスの位置にそろいます。

css
/* 入力欄の中の文字を、マスの行にそろえ、スクロールしても線がずれないようにする */
.grid-note {
  font-size: 16px;
  line-height: 24px;
  padding: 0 0 0 24px;
  border: 1px solid #9fdcea;
  background-image:
    linear-gradient(to right, #9fdcea 1px, transparent 1px),
    linear-gradient(to bottom, #9fdcea 1px, transparent 1px);
  background-size: 24px 24px;
  background-attachment: local;
}

最後の background-attachment が大事です。初期値の scroll では、背景は要素の枠に対して固定され、中の文字をスクロールしても動きません。local にすると、背景が中身と一緒にスクロールします。

高さ120px(5行分)の入力欄に10行の文字を入れ、36px(1行半)スクロールして比べました。scroll では、スクロールしても横線は内側の上端から 0、24、48… の位置にあり、文字の行と半マスずれて、線が文字の真ん中を通りました。local では、横線が 12、36、60… の位置に動き、文字の行は上下の横線の間に収まりました。

上下2段の比較。どちらも「2行目のメモです」から「7行目のメモです」までが見える位置にスクロールしたマス目の入力欄。上の「background-attachment: scroll(初期値)」では、横線が文字の真ん中を通っている。下の「background-attachment: local」では、文字が横線と横線の間に収まっている
1行半スクロールしたとき、local(下)だけ文字が線の間に収まる

対応しているブラウザ

MDN の互換性データ(@mdn/browser-compat-data 8.1.8)で、この記事で使っている機能を調べました。

機能 Chrome Firefox Safari
linear-gradient() 26 16 7
背景画像を複数重ねる 1 3.6 1.3
background-size 3 4 5
background-attachment: local 1 25 15.4
round() 125 118 15.4

Safari の background-attachment: local は、5から12.1までは対応していて、13から15.3までは値を受け付けても効かなかったと記録されています。

​Posted by 86