
2026/02/16 3:04
**モダンCSSコードスニペット:2015風の書き方はやめましょう** - **CSSカスタムプロパティ(変数)を使う** - 色・余白・タイポグラフィなど、再利用可能な値を `:root` に定義します。 - 例: ```css :root { --primary-color: #3498db; --spacing-unit: 1rem; } ``` - **FlexboxとGridを早期に活用** - 一次元レイアウトは Flexbox、二次元構造には Grid を選びます。 - 例: ```css .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--spacing-unit); } ``` - **ミックスイン・関数でスタイルを DRY に保つ** - Sass/SCSS や CSS‑in‑JS を使って再利用可能なミックスインを作成します。 - 例: ```scss @mixin card($bg, $shadow: 0 4px 6px rgba(0,0,0,.1)) { background: $bg; box-shadow: $shadow; border-radius: .5rem; padding: var(--spacing-unit); } .card { @include card(#fff); } ``` - **モバイルファーストのメディアクエリを採用** - モバイル向けにベーススタイルを書き、徐々に拡張します。 - 例: ```css .container { width: 100%; max-width: 1200px; margin: auto; padding: var(--spacing-unit); } @media (min-width: 768px) { .container { padding: calc(var(--spacing-unit) * 2); } } ``` - **国際化に対応する論理プロパティを使う** - `margin-left` / `padding-right` を論理プロパティに置き換えます。 - 例: ```css .box { margin-inline-start: var(--spacing-unit); padding-block-end: var(--spacing-unit); } ``` - **過度なセレクタの特異性を避ける** - 特異性は低く抑え、クラス名やコンポーネントスコープで管理します。 - 例: ```css /* ではなく */ body .header nav ul li a { ... } /* 使用するのは */ .nav-link { ... } ``` - **モダンユニット(`rem`, `em`, `%`, `vh/vw`)を活用** - タイポグラフィや余白をルートフォントサイズに相対的にスケールします。 - 例: ```css html { font-size: 62.5%; } /* 1rem = 10px */ h1 { font-size: 3rem; } ``` - **CSS Houdiniでカスタムスタイリングを実装** - `@property`, `paint()`, `layout()` API を使ってブラウザレンダリングパイプラインにフックします。 - 例: ```css @property --border-radius { syntax: "<length>"; inherits: false; initial-value: 0px; } .fancy-box { border-radius: var(--border-radius); } ``` - **アクセシビリティを念頭に置く** - コントラスト、フォーカス状態、レスポンシブテキストサイズを確保します。 - 例: ```css a:focus { outline: 3px solid #ffbf47; outline-offset: 2px; } ``` これらのモダンなプラクティスに従えば、CSS はよりクリーンで保守しやすく、将来にも耐えうるものになります。
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
要約
この記事では、増え続けるネイティブCSS機能が、開発者が従来スタイリング、レイアウト、およびUIロジックのために使用してきた多くのJavaScript駆動またはプリプロセッサパターンを置き換える方法を示しています。主な機能は次のとおりです:
- 範囲スタイルクエリ –
は、複数の@container style(--progress > 50%)
呼び出しなしに要素がコンテナ状態に反応できるようにします。style() - 固定・スナップされた要素のスタイリング –
は、固定UI用のJSスクロールリスナーを置き換えます。@container scroll‑state(stuck: top) - 型付き属性値 –
は、JavaScriptによるデータセット操作を排除します。width: attr(data-pct type(<percentage>)); - インライン条件スタイル – CSS の
関数は、スクリプトでクラスを切り替える必要をなくします。if() - ネイティブ関数で再利用可能なロジック –
は Sass のミックスインを代替します。@function --fluid(--min, --max) { @return clamp(...); } - 丸み以外の角形状 –
は、新しい視覚スタイルをネイティブに提供します。border‑radius: 2em; corner-shape: squircle; - レスポンシブクリップパス –
は SVG または長いclip-path: shape(from 0% 100%, …);
文字列を置き換えます。path() - スクロールスパイ機能 –
のようなセレクタは、CSSだけでスクロール追跡を可能にします。nav a:target-current { color: var(--accent); } - 利用可能スペースの埋め込み –
はwidth: stretch;
やオーバーフロー作業を不要にします。calc(100% - 40px) - 段階的アニメーション –
は多くのtransition-delay: calc(0.1s * (sibling-index() - 1));
規則を排除します。nth-child() - 疑似要素によるカルーセルナビゲーション –
とマーカーは、JSライブラリに対する軽量代替手段です。.carousel::scroll-button(right) { content: "➡"; } - 垂直テキストセンタリング –
はパディングハックなしで真の光学的センタリングを提供します。text-box: trim-both cap alphabetic;
これらの機能は、近年のブラウザでますますサポートされており、開発者はJavaScriptロジックを減らしたり排除したり、バンドルサイズを縮小し、保守性を簡素化し、よりレスポンシブで維持可能なスタイルシートを作成できます。この傾向は、ウェブ業界全体でインタラクティブUIパターンに対するCSSファーストアプローチへと進むことを示しています。
本文
ブラウザ互換性
-
複数ブロックを使わない範囲スタイルクエリ
- 従来
/* 複数の style() ブロック */ @container style(--p: 51%) {} @container style(--p: 52%) {} /* …各値ごとに */ - 現代
@container style( --progress > 50%) { .bar { … } }
- 従来
-
JavaScript を使わずに固定&スナップ要素をスタイリング
- 従来
window.addEventListener('scroll', () => { /* ポジション確認 */ }); - 現代
@container scroll-state( stuck: top ) { .header { … } }
- 従来
-
JavaScript を使わずに属性値を型付きで取得
- 従来
// JS で dataset.el.style.width = el.dataset.pct + '%'; - 現代
.bar { width: attr( data-pct type(<percentage>) ); }
- 従来
-
JavaScript を使わずに条件付きインラインスタイル
- 従来
// JavaScript でトグル el.classList.toggle('primary', isPrimary); - 現代
.btn { background: if( style(--variant: primary): blue; else: gray ); }
- 従来
-
Sass のミックスインを使わずに再利用可能な CSS ロジック
- 従来
// Sass 関数 @function fluid($min, $max) { @return clamp(...); } - 現代
@function --fluid( --min, --max ) { @return clamp(...); }
- 従来
-
丸角以外のコーナー形状
- 従来
.card { clip-path: polygon( … /* 20+ points */ ); } - 現代
.card { border-radius: 2em; corner-shape: squircle; }
- 従来
-
SVG を使わずにレスポンシブクリップパス
- 従来
.shape { clip-path: path( 'M0 200 L100 0…' ); } - 現代
.shape { clip-path: shape( from 0% 100%, … ); }
- 従来
-
IntersectionObserver を使わずにスクロールスパイ
- 従来
const observer = new IntersectionObserver(cb); /* 15+ 行の JS */ - 現代
nav a:target-current { color: var(--accent); }
- 従来
-
calc のワークアラウンドを使わずに利用可能なスペースを埋める
- 従来
.full { width: calc(100% - 40px); /* または width: 100% と overflow */ } - 現代
.full { width: stretch; } /* コンテナを満たし、余白は維持 */
- 従来
-
nth‑child のハックを使わずに staggered アニメーション
- 従来
li:nth-child(1) { --i: 0; } li:nth-child(2) { --i: 1; } li:nth-child(3) { --i: 2; } /* 各アイテムごとに… */ - 現代
li { transition-delay: calc(0.1s * (sibling-index() - 1)); }
- 従来
-
JavaScript ライブラリを使わずにカルーセルナビゲーション
- 従来
// Swiper.js や Slick carousel new Swiper('.carousel', { navigation: { /* … */ }, pagination: { /* … */ } }); - 現代
.carousel::scroll-button(right) { content: "➡"; } .carousel li::scroll-marker { content: ''; }
- 従来
-
パディングハックを使わずに縦書きテキストのセンタリング
- 従来
.btn { padding: 10px 20px; /* 中央寄せがずれる、上/下を微調整 */ padding-top: 8px; /* ハック */ } - 現代
h1, button { text-box: trim-both cap alphabetic; } /* 本当の光学的センタリング */
- 従来
-
JavaScript イベントを使わずにホバー時ツールチップ
- 従来
// JS: mouseenter + mouseleave btn.addEventListener('mouseenter', () => showTooltip()); /* + focus, blur, positioning */ - 現代
<button interestfor="tip">Hover me</button> <div id="tip" popover=hint> Tooltip content </div>
- 従来
-
onclick ハンドラを使わずにモーダルコントロール
- 従来
<button onclick="document.querySelector('#dlg').showModal()">Open</button> - 現代
<button commandfor="dlg" command="show-modal">Open</button> <dialog id="dlg">…</dialog>
- 従来
-
クリックアウト側リスナを使わずにダイアログのライトディスポーズ
- 従来
// JS: backdrop のクリックを監視 dialog.addEventListener('click', (e) => { /* 境界チェック */ }) - 現代
<dialog closedby="any"> Click outside to close </dialog>
- 従来
-
JavaScript ライブラリを使わずにカスタマイズ可能なセレクト
- 従来
// Select2 や Choices.js new Choices('#my-select'); /* DOM を再構築 */ - 現代
select, select ::picker(select) { appearance: base-select; }
- 従来
-
sRGB を超える鮮やかな色
- 従来
.hero { color: rgb(200, 80, 50); } /* sRGB のみ、P3 では薄くなる */ - 現代
.hero { color: oklch(0.7 0.25 29); /* または color(display-p3 1 0.2 0.1) */ }
- 従来
-
Sass 関数を使わずに色変種
- 従来
/* Sass: lighten($brand, 20%), darken($brand, 10%) */ .btn { background: #e0e0e0; } - 現代
.btn { background: oklch(from var(--brand) calc(l + 0.2) c h); }
- 従来
-
JavaScript を使わずに複数行テキストの省略
- 従来
/* JS: 文字/単語で切り、"..." を追加 */ .card-title { overflow: hidden; } - 現代
.card-title { display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; }
- 従来
-
float ハックを使わずにドロップキャプ
- 従来
.drop-cap::first-letter { float: left; font-size: 3em; line-height: 1; } - 現代
.drop-cap::first-letter { initial‑letter: 3; }
- 従来
-
四角形のプロパティを省略して位置指定
- 従来
.overlay { top: 0; right: 0; bottom: 0; left: 0; } - 現代
.overlay { position: absolute; inset: 0; }
- 従来
-
IntersectionObserver を使わずに遅延レンダリング
- 従来
// JS IntersectionObserver new IntersectionObserver((entries) => { /* レンダリング */ }).observe(el); - 現代
.section { content‑visibility: auto; contain‑intrinsic‑size: auto 500px; }
- 従来
-
JavaScript のトグルを使わずにドロップダウンメニュー
- 従来
.menu { display: none; } .menu.open { display: block; } /* + JS: click, clickOutside, ESC, aria */ - 現代
button[popovertarget=menu] {} #menu[popover] { position: absolute; }
- 従来
-
JavaScript を使わずにツールチップの位置決め
- 従来
/* Popper.js / Floating UI: rect 計算、position: fixed、スクロール時更新 */ .tooltip { position: fixed; } - 現代
.trigger { anchor‑name: --tip; } .tooltip { position‑anchor: --tip; top: anchor(bottom); }
- 従来
-
BEM 命名を使わずにスコープ付きスタイル
- 従来
// BEM: .card__title, .card__body.card__title { … } .card__body { … } /* または CSS Modules / styled‑components */ - 現代
@scope (.card) { .title { font-size: 1.25rem; } .body { color: #444; } }
- 従来
-
JavaScript を使わずに型付きカスタムプロパティ
- 従来
// --hue は文字列、アニメーション不可 :root { --hue: 0; } hsl(var(--hue), …) /* 変化が無い */ - 現代
@property --hue { syntax: "<angle>"; inherits: false; initial‑value: 0deg; }
- 従来
-
ショートハンドを使わずに独立したトランスフォーム
- 従来
.icon { transform: translateX(10px) rotate(45deg) scale(1.2); } /* 一つだけ変えると全書き直し */ - 現代
.icon { translate: 10px 0; rotate: 45deg; scale: 1.2; }
- 従来
-
ワークアラウンドを使わずに display:none をアニメーション
- 従来
// transitionend 後に display:none el.addEventListener('transitionend', …); visibility + opacity + pointer‑events - 現代
.panel { transition: opacity .2s, overlay .2s; transition-behavior: allow-discrete; } .panel.hidden { opacity: 0; display: none; }
- 従来
-
JavaScript タイミングを使わずにエントリーアニメーション
- 従来
// requestAnimationFrame(() => { el.classList.add('visible'); }); - 現代
.card { transition: opacity .3s, transform .3s; } .card { @starting‑style { opacity: 0; transform: translateY(10px); } }
- 従来
-
フレームワークを使わずにページ遷移
- 従来
// Barba.js や React Transition Group Barba.init({ … }) transition hooks + duration state - 現代
document.startViewTransition(() => updateDOM()); .hero { view‑transition‑name: hero; }
- 従来
-
カルーセルライブラリを使わずにスクロールスナップ
- 従来
// Slick, Swiper, or scroll/touch JS $('.carousel').slick({ … }) touchstart / scroll handlers - 現代
.carousel { scroll‑snap‑type: x mandatory; } .carousel > * { scroll‑snap‑align: start; }
- 従来
-
手動改行を使わずにバランスの取れた見出し
- 従来
// <br> を手動で入れるか Balance-Text.js h1 { text-align: center; } .balance-text /* JS lib */ - 現代
h1, h2 { text-wrap: balance; }
- 従来
-
見えないテキストを使わずにフォント読み込み
- 従来
@font-face { … } /* デフォルト:ロードまで不可視 */ - 現代
@font-face { font-family: "MyFont"; font-display: swap; }
- 従来
-
複数ファイルを使わずに多重ウェイト
- 従来
@font-face { font-weight: 400; } @font-face { font-weight: 700; } /* 4+ ファイル */ - 現代
@font-face { font-family: "MyVar"; src: url("MyVar.woff2"); font-weight: 100 900; }
- 従来
-
追加 CSS を使わずにダークモードのデフォルト
- 従来
@media (prefers-color-scheme: dark) { input, select, textarea { … } } - 現代
:root { color‑scheme: light dark; }
- 従来
-
値を重複させずにダークモードカラー
- 従来
@media (prefers-color-scheme: dark) { color: #eee; } - 現代
color: light-dark(#111, #eee); color‑scheme: light dark;
- 従来
-
複雑なセレクタを使わずに低特異度リセット
- 従来
.reset ul, .reset ol { … } /* (0,0,1) の特異度でも勝る */ - 現代
:where(ul, ol) { margin: 0; padding-inline-start: 1.5rem; }
- 従来
-
左/右を使わずに方向感知レイアウト
- 従来
margin-left: 1rem; padding-right: 1rem; [dir="rtl"] .box { margin-right: … } - 現代
margin-inline-start: 1rem; padding-inline-end: 1rem; border-block-start: 1px solid;
- 従来
-
行番号を使わずにグリッド領域名
- 従来
float: left; /* clearfix, margins */ grid-column: 1 / 3; grid-row: 2; - 現代
.layout { display: grid; grid-template-areas: "header header" "sidebar main" "footer footer"; }
- 従来
-
親トラックを重複させずに入れ子グリッドの整列
- 従来
.child-grid { grid-template-columns: 1fr 1fr 1fr; /* 親と同じトラックを重複 */ } - 現代
.child-grid { display: grid; grid-template-columns: subgrid; }
- 従来
-
JavaScript ライブラリを使わずにモーダルダイアログ
- 従来
.overlay { position: fixed; z-index: 999; } /* + JS: 開閉、ESC、フォーカストラップ */ - 現代
dialog { padding: 1rem; } dialog::backdrop { background: rgb(0 0 0 / .5); }
- 従来
-
再構築せずにフォームコントロールをスタイリング
- 従来
appearance: none; /* + 20+ 行のカスタムボックス/境界/背景 */ - 現代
input[type="checkbox"], input[type="radio"] { accent-color: #7c3aed; }
- 従来
-
セレクタを重複せずにグループ化
- 従来
.card h1, .card h2, .card h3, .card h4 { margin-bottom: 0.5em; } - 現代
.card :is(h1, h2, h3, h4) { margin-bottom: 0.5em; }
- 従来
-
マウスユーザーを煩わせずにフォーカススタイル
- 従来
:focus { outline: 2px solid blue; } /* マウスクリック時も表示、あるいは人が削除(a11y 失敗) */ - 現代
:focus-visible { outline: 2px solid var(--focus-color); }
- 従来
-
!important を使わずに特異度を制御
- 従来
.card .title { … } .page .card .title { … } .page .card .title.special { color: red !important; } - 現代
@layer base, components, utilities; @layer utilities { .mt-4 { margin-top: 1rem; } }
- 従来
-
プリプロセッサを使わずにテーマ変数
- 従来
// Sass: $primary: #7c3aed; /* コンパイル時に静的に btn { background: $primary; } */ - 現代
:root { --primary: #7c3aed; } .btn { background: var(--primary); }
- 従来
-
メディアクエリを使わずに流動的タイポグラフィ
- 従来
h1 { font-size: 1rem; } @media (min-width: 600px) { h1 { font-size: 1.5rem; } } @media (min-width: 900px) { h1 { font-size: 2rem; } } - 現代
h1 { font-size: clamp(1rem, 2.5vw, 2rem); }
- 従来
-
マージンハックを使わずに要素間のスペース
- 従来
.grid > * { margin-right: 16px; } .grid > *:last-child { margin-right: 0; } - 現代
.grid { display: flex; gap: 16px; }
- 従来
-
パディングハックを使わずにアスペクト比
- 従来
.wrapper { padding-top: 56.25%; position: relative; } .inner { position: absolute; inset: 0; } - 現代
.video-wrapper { aspect-ratio: 16 / 9; }
- 従来
-
JavaScript スクロールリスナを使わずにヘッダー固定
- 従来
// JS: scroll listener + getBoundingClientRect// then add/remove .fixed class.header.fixed { position: fixed; } - 現代
.header { position: sticky; top: 0; }
- 従来
-
ライブラリを使わずにスクロールリンクアニメーション
- 従来
// JS + IntersectionObserver observer.observe(el) el.style.opacity = … - 現代
animation-timeline: view(); animation-range: entry; /* 純 CSS、GPU アクセラレーション */
- 従来
-
Sass や Less を使わずにネストされたセレクタ
- 従来
// Sass コンパイラ必須 nav { & a { color: #888; } } - 現代
.nav { & a { color: #888; } } /* 純粋な .css、ビルド不要 */
- 従来
-
メディアクエリを使わずにレスポンシブコンポーネント
- 従来
@media (max-width: 768px) { .card { … } } /* ビューポート、コンテナではない */ - 現代
@container (width < 400px) { .card { flex-direction: column; } }
- 従来
-
プリプロセッサを使わずに色合成
- 従来
// Sass 必須 $blend: mix( $blue, $pink, 60% ); - 現代
background: color-mix(in oklch, #3b82f6, #ec4899);
- 従来
-
JavaScript を使わずに親要素を選択
- 従来
// JavaScript 必須 el.closest('.parent') .classList.add(…) - 現代
.card:has(img) { grid-template: auto 1fr; }
- 従来
-
transform ハックを使わずに中央揃え
- 従来
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); - 現代
.parent { display: grid; place-items: center; }
- 従来