数行のCSSで、UIデザインの使い心地を向上させるテクニックのまとめ
Post on:2026年6月4日
sponsorsr
優れたインターフェイスは、小さな要素が積み重なって、素晴らしいユーザーエクスペリエンスが生み出されます。数行のCSSで、インターフェイスの使い心地を向上させるテクニックを紹介します。
生成AIのスキルとしてコーディングアシスタント(Claude Code、Codexなど)に使うこともできます。

Details that make interfaces feel better
Details that make interfaces feel better -GitHub
- はじめに
- 1. テキストの折り返し(text-wrap: balance / pretty)
- 2. ネストされた角丸
- 3. コンテキストに応じたアイコンアニメーション
- 4. macOSでフォントをくっきり細く表示させる方法
- 5. 動的に表示される数字
- 6. 中断可能なアニメーション
- 7. 複数のアニメーションを同時に適用しない
- 8. アニメーション終了するときの演出
- 9. 数学的ではなく、視覚的に位置を合わせる
- 10. ボーダーの代わりにシャドウを使用
- 11. 奥行きが生まれる画像の輪郭
- 12. 機能を実行する前と後のデザイン
はじめに
ここで紹介する小さな工夫は、スキルとしてAIコーディングアシスタント(Claude Code、Codexなど)に「優れたインターフェースを構成する細かなディテール」を教えることもできます。MITライセンスで、商用プロジェクトでも無料で利用できます。
Npx経由でインストールできます。
|
1 |
npx skills add jakubkrehel/make-interfaces-feel-better |
インストールが完了すると、ClaudeはUIコンポーネントの構築、コードのレビュー、アニメーションの実装を行う際に、これらの原則を自動的に適用します。
1. テキストの折り返し(text-wrap: balance / pretty)
テキストの見栄えを改善する方法として、text-wrap: balance;を設定する方法があります。テキストを行全体に均等に配置し、見出しや短いテキストブロックで単語が孤立してしまうのを防ぐことができます。text-wrap: balance;は、6行以下(Chromium)または10行以下(Firefox)のテキストブロックのみ有効です。バランス調整アルゴリズムは計算コストが高いため、ブラウザは短いテキストに限定されています。
|
1 2 3 4 5 6 7 8 9 |
/* Good - 短いテキストの行の長さが均一であること */ h1, h2, h3 { text-wrap: balance; } /* Bad - 長い段落のバランスが悪い */ .article-body p { text-wrap: balance; } |

text-wrap: balance;を使用したデモ
また、文末に単語が残ってしまうのを防ぐためにtext-wrap: pretty;を設定することもできます。段落全体の改行位置を調整することで、孤立した単語(行の最後にぽつんと残った単語)の発生を防ぎます。balanceと異なるのは、行の長さを均等にするのではなく、最後の行が不自然に短くならないように調整することです。行数の制限はなく、どの長さのテキストでも機能します。
そのため、短文から中程度の長さのテキスト(段落・説明文・キャプション・リスト項目・カードテキストなど)にデフォルトとして設定しておくことをお勧めします。非常に長いテキスト(10行以上)の場合は、balanceとprettyのどちらも使用しないでください。ブラウザのデフォルトの改行で十分であり、不要なレイアウト処理の負荷を避けることができます。
|
1 2 3 4 |
/* 良い点 ― 説明文、キャプション、短い段落 */ p, li, figcaption, blockquote { text-wrap: pretty; } |

text-wrap: pretty;を使用したデモ
balanceとprettyの使い分けをまとめると下記の通りです。
- 見出し、タイトルなど、均等なレイアウトが重要な場合:
text-wrap: balance; - 短~中程度のテキスト(段落、説明文、キャプション、UIテキスト):
text-wrap: pretty; - 長文(10行以上)、コードブロック、整形済みテキスト:どちらも適用しない(デフォルトのままにする)
2. ネストされた角丸
同心円状のオフセットは、要素を入れ子にする際に視覚的にバランスを整えるために使用されるテクニックです。これはインターフェイスの使いやすさを向上させる上で重要な概念の一つですが、見過ごされがちです。

ネストされた角丸
ネストされた角丸の正しい値を計算するルールがあり、それは非常にシンプルです。外側の半径は内側の半径に要素間のパディングを加えた値と等しくします。
|
1 |
外側半径 = 内側半径 + パディング |
内側の角丸が12pxで、パディングが8px;の場合は、下記のようになります。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Good - 同心円状の半径 */ .card { border-radius: 20px; /* 12 + 8 */ padding: 8px; } .card-inner { border-radius: 12px; } /* Bad — 両方とも同じ半径 */ .card { border-radius: 12px; padding: 8px; } .card-inner { border-radius: 12px; } |
驚くべきことに、いまだに多くのアプリやインターフェイスでこのルールが使用されず、その代わりに外側と内側の半径を同じ値にしているのを見かけます。このルールは、入れ子になった要素が違いに近接している場合に特に有用です。パディングが24pxより大きい場合は、レイヤーを別のサーフェスとして扱い、厳密な同心円計算をするのではなく、それぞれの半径を個別に設定します。

ネストされた角丸
3. コンテキストに応じたアイコンアニメーション
アイコンがコンテキストに応じて(マウスオーバーやステータス変化など)表示・非表示になる場合、単に表示と非表示を切り替えるのではなく、不透明度・拡大縮小・ぼかし(opacity, scale, blur)などのアニメーション効果を加えてください。

コンテキストに応じたアイコンアニメーション
CSSだけでもアニメーション効果を実装できますが、個人的にはスプリングアニメーションを簡単に使えるので、Motionを使うことを好みます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import { AnimatePresence, motion } from "motion/react"; function IconButton({ isActive, icon: Icon }) { return ( <button> <AnimatePresence mode="popLayout"> <motion.span key={isActive ? "active" : "inactive"} initial={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, scale: 0.25, filter: "blur(4px)" }} transition={{ type: "spring", duration: 0.3, bounce: 0 }} > <Icon /> </motion.span> </AnimatePresence> </button> ); } |
4. macOSでフォントをくっきり細く表示させる方法
macOSでは、デフォルトでテキストが意図したよりも太く表示されることがあります。この問題を解決するには、ルート要素にアンチエイリアス処理を適用することで、すべてのテキストがより鮮明で細く表示されるようになります。

macOSでフォントをくっきり細く表示させる方法
下記のCSSをルート要素に追加することで、アプリ内のすべてのテキスト要素に適用されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* Good - ルートに設定 */ html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } /* Bad - 要素ごとに適用され、一貫性がない */ .heading { -webkit-font-smoothing: antialiased; } .body { /* no smoothing → heavier than heading */ } |
5. 動的に表示される数字
数値が動的に更新される場合(カウンター・価格・タイマー・表の列など)、すべての桁の幅を均一するにはtabular-numsを使用します。これにより、値が変更されてもレイアウトが崩れるのを防ぐことができます。
|
1 2 3 |
.counter { font-variant-numeric: tabular-nums; } |

動的に表示される数字
ただし、一部のフォント(Interなど)ではこのプロパティによって数字の見た目が変化します。具体的には数字の「1」が幅広になり、中央に配置されます。これは想定された動作であり、通常は配置の面で望ましいものではありますが、使用するフォントで正しく表示されるか必ず確認してください。
6. 中断可能なアニメーション
ユーザーは操作の途中で意図を変えることがあります。たとえば、ドロップダウンを開いたときにアニメーションが終了する前に別の操作をしたいと考える場合があります。その際にアニメーションが中断できないと、インターフェースは不具合があるように感じられます。iOSではまさにこの理由からアニメーションの中断機能が広く採用されています。
アニメーションの中断は、CSSのトランジションとキーフレームアニメーションでは挙動が異なります。トランジションは最新の状態に向かって補間的に動作し中断が可能ですが、キーフレームアニメーションは固定されたタイムラインに沿って実行されるので、アニメーションが一度開始されると目標状態が変更させることはできません。

中断可能なアニメーション
CSSのトランジションとキーフレームアニメーションのどちらを使うか判断する際の目安として、トランジションはインタラクションに最適で、キーフレームアニメーションは一度だけ実行される段階的なシーケンスに最適であるということを覚えておくとよいでしょう。

中断可能なアニメーション

中断可能なアニメーション

中断可能なアニメーション
7. 複数のアニメーションを同時に適用しない
大きなコンテナを表示するアニメーションでは、opacity, blur, translateYなどを組み合わせることがよくあります。大きなコンテナは一度にすべてのアニメーションを適用するのではなく、アニメーション対象となる要素を小さな単位に分割し、それぞれ個別にアニメーションさせる方が効果的です。

複数のアニメーションを同時に適用しない
たとえば、最初のバリアントでは、タイトル・説明・ボタンを含む単一のコンテナをアニメーション化します。2番目のバリアントでは、タイトル・説明・ボタンを個別にアニメーション化し、各セクション間に100ミリ秒の遅延を設けます。
|
1 2 3 4 5 6 7 8 9 |
<div className="animate-enter" style={{ "--stagger": 1 }}> <Title /> </div> <div className="animate-enter" style={{ "--stagger": 2 }}> <Description /> </div> <div className="animate-enter" style={{ "--stagger": 3 }}> <Buttons /> </div> |
3番目のバリエーションでは、タイトルを複数のスパンに分割することでアニメーション化します。各スパンには単語が1つずつ含まれており、それぞれ80ミリ秒の間隔を空けて個別にアニメーション化されます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@keyframes enter { from { transform: translateY(8px); filter: blur(5px); opacity: 0; } } .animate-enter { animation: enter 800ms cubic-bezier(0.25, 0.46, 0.45, 0.94) both; animation-delay: calc(var(--delay, 0ms) * var(--stagger, 0)); } .animate-enter-individual-title { --delay: 80ms; } |
説明文は単一のブロックとして表示され、ボタンもコンテナ全体ではなく個別にアニメーション表示されます。
8. アニメーション終了するときの演出
アニメーションが終了するときは、開始時のアニメーションよりも控えめな方が効果的な場合が多いです。

アニメーション終了するときの演出
画面から消えていく要素は、画面に入ってくる要素ほど多くの動きや注意を必要としません。たとえば、消える際のy座標はcalc(-100% - 4px)となっており、これはコンテナの高さにパディングを加えた値です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<motion.div key="menu" className="container" initial={{ opacity: 0, y: "calc(-100% - 4px)", filter: "blur(4px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} exit={{ opacity: 0, y: "calc(-100% - 4px)", filter: "blur(4px)", }} transition={{ type: "spring", duration: 0.45, bounce: 0 }} /> |
さらに控えめにするには、固定値として-12pxを使用します。方向を示すためにある程度の動きは残しておく必要があります。アニメーションで完全に削除することはお勧めしません。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
<motion.div key="menu" className="container" initial={{ opacity: 0, y: "calc(-100% - 4px)", filter: "blur(4px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} exit={{ opacity: 0, y: "-12px", filter: "blur(4px)", }} transition={{ type: "spring", duration: 0.45, bounce: 0 }} /> |
こうすることで、アニメーションの終了時がより滑らかになり、違和感が少なくなり、開始時のアニメーションほど注意を払う必要がなくなります。
9. 数学的ではなく、視覚的に位置を合わせる
要素を数学的に配置すると、たいていの場合はうまく機能しますが、見た目が不自然になる場合もあります。そのような場合は数学的ではなく、代わりに視覚的に配置させるのが最善です。

数学的ではなく、視覚的に位置を合わせる
たとえば、ボタンにテキストとアイコンを配置する場合、視覚的に整列されるにはアイコンの横にすこしだけ余白を設ける方がよいでしょう。
|
1 2 3 4 5 6 7 8 9 |
/* Good — 視覚的に中央配置 */ .play-button svg { margin-left: 2px; /* 三角形の形状を考慮して右にずらす */ } /* Bad — 数学的には中央だが、ずれて見える */ .play-button svg { /* 調整なし */ } |

数学的ではなく、視覚的に位置を合わせる
これはアイコンでよく起こる問題です。多くのアイコンはすでにこの点を考慮してデザインされていますが、中には視覚的に整列させる必要がある形状もあります。私は通常、コンテナに応じてマージンやパディングを追加することで解決しています。

数学的ではなく、視覚的に位置を合わせる
アイコンによっては視覚的な重みが均一ではない場合があります。最適な解決方法はSVGを直接調整することです。そうすれば、コンポーネント内のコードに余計なマージンやパディングを追加する必要はなくなります。
10. ボーダーの代わりにシャドウを使用
要素にボーダーを与える加える代わりに、奥行きを与える控えめなボックスシャドウが使用されているのを見かけます。

ボーダーの代わりにシャドウを使用
上記のシャドウは、3つの異なるシャドウで構成されています。1つ目は1ピクセルの境界リングとして機能し、2つ目は微妙なリフト効果を加え、3つ目は周囲の奥行き感を表現します。
|
1 2 3 4 5 6 |
.border-shadow { box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.06), 0px 1px 2px -1px rgba(0, 0, 0, 0.06), 0px 2px 4px 0px rgba(0, 0, 0, 0.04); } |
ダークモードでは、単一の白いリングに簡略化します。レイヤー化された深度シャドウは暗い背景では表示されません。
|
1 2 |
--shadow-border: 0 0 0 1px rgba(255, 255, 255, 0.08); --shadow-border-hover: 0 0 0 1px rgba(255, 255, 255, 0.13); |
ホバーすると、ボックスシャドウはホバー状態と同じで、わずかに暗くなります。シャドウ間の遷移を実現するにはtransition-shadowにボックスシャドウを追加します。
|
1 2 3 4 5 6 |
.border-shadow { box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.08), 0px 1px 2px -1px rgba(0, 0, 0, 0.08), 0px 2px 4px 0px rgba(0, 0, 0, 0.06); } |
シャドウを使用する利点はまだあります、画像や複数の色を背景として使用する場合です。シャドウは透明度を利用するため汎用性が高く、どんな背景にもよく馴染みます。

ボーダーの代わりにシャドウを使用
単色のボーダーは想定されている背景以外の背景に使用すると、うまく機能しないことがあります。
11. 奥行きが生まれる画像の輪郭
画像に透明度の低い1ピクセルの控えめなアウトラインを追加します。これは特に他の要素がボーダーやシャドウを使用しているデザインシステムにおいて一貫した奥行き感が生まれます。

奥行きが生まれる画像の輪郭
アウトラインの色は、ライトモードではピュアブラック(rgba(0, 0, 0, 0.1))を使用し、ダークモードではピュアホワイト(rgba(255, 255, 255, 0.1))を使用します。この色はアクセントカラーに合わせてはいけません。アウトラインは中立的な区切り線なので、テーマに沿った要素ではありません。
|
1 2 3 4 5 6 7 8 |
.border-overlay { outline: 1px solid rgba(0, 0, 0, 0.1); outline-offset: -1px; } .dark .border-overlay { outline-color: rgba(255, 255, 255, 0.1); } |
私は主に、他の要素も境界線を使用しているデザインシステムでこれを使います。

奥行きが生まれる画像の輪郭
borderではなく、outlineを使用する理由は、レイアウトに影響を与えないからです(幅や高さを追加されない)。outline-offset: -1px;とするこおtで、画像本来のサイズに収まるように内側に配置されます。
12. 機能を実行する前と後のデザイン
機能を実行する前と後で、コンテンツが同じ構造で同じデザインの方が明らかに操作感がよくなります。

機能を実行する前と後のデザイン
上記では、タイマーを起動すると、表示される数字によって数字のエリアとボタンの位置が動きます。また、右上リロード時のアニメーション、リンクのコピーなども同様に改善されています。
sponsors












