GODRICH

プロフィールカード デザイン 9選 — コピペOKのスタイル集

プロフィールカード デザインとは、アバター・名前・肩書き・数値・フォローボタンという決まった中身に、どんな質感を着せるかを決める作業です。9つは内側の構造がすべて同じで、表面と動きだけが違います。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

前半の6つはカード1枚の表面だけを変えます。中身の項目も文言もボタンの動きも共通で、違うのはスタイルと、質感のために足した数個の要素だけです。並び順は光をどれだけ扱うかの順です。後ろにあるものをぼかして映すガラス(01)から始まり、光の代わりに影を図形として使うネオブルータリズム(02)、内と外の影を重ねて厚みを作る粘土(03)、影をすべて捨てて線だけ残したミニマル(04)、枠だけが光るグラデーション(05)、暗闇でにじむネオン(06)へと進みます。後半の3つは表面ではなく構造をひとつずつ足します — 裏面(07)、時間(08)、隣(09)です。9つとも、フォローボタンは実際に aria-pressed を切り替えて文字とアイコンを差し替えます。

01グラスモーフィズム

カードの後ろで色のにじみが2つゆっくり流れ、半透明のカードがその色をぼかして映します。曇りガラスを作るのは backdrop-filter の1行ですが、その後ろにぼける対象が実際にないと効果は見えません。

backdrop-filterradial-gradientaria-pressed
.gf__card {
  position: absolute;
  inset: 0;
  background: rgba(255, 255, 255, .30);
  border: 1px solid rgba(255, 255, 255, .62);
  border-radius: $r-card;
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  color: $subject-ink;
}
.gf__blob--a {
  top: -24px; left: -30px;
  width: 140px; height: 140px;
  background: radial-gradient(circle at 38% 38%, $subject-pink, rgba($subject-pink, 0) 72%);
}

02ネオブルータリズム オフセット

太い枠と右下にずらした単色の影が、カードを切り抜いた紙のように浮かせます。押すとカードは影の位置にぴったり沈み、影はその下に消えます。

box-shadowtranslatearia-pressed
.nb__card {
  border: 3px solid $subject-ink;
  border-radius: $r-card;
  box-shadow: 8px 8px 0 $subject-ink;
}
@keyframes nb-press {
  0%, 100% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
  9% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
  20% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
  46% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
  55% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
  68% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
}

03クレーモーフィズム

影を3枚重ねることで、カードがふくらんだ粘土のように見えます。外へ広がる影ひとつに、内側の明るいハイライトと内側の暗いくぼみを足した組み合わせが、もちっとした厚みになります。

box-shadowinsetaria-pressed
.cp__card {
  border-radius: $r-card;
  background: rgba($subject-pink, .28);
  box-shadow:
    0 18px 30px rgba(23, 20, 26, .14),
    inset 6px 6px 12px rgba(255, 255, 255, .9),
    inset -6px -6px 12px rgba(23, 20, 26, .10);
}
@keyframes cp-breathe {
  0%, 100% { transform: scale(1); }
  44% { transform: scale(1.12); }
  88% { transform: scale(1); }
}

04ミニマルライン

白い背景に白いカードなので、背景との差がありません。1pxの枠線と余白だけで形を作り、名前とボタンの下に左から引かれる下線だけが唯一の飾りです。

scaleXtransform-originaria-pressed
.ml__line {
  display: block;
  height: 2px;
  background: $subject-ink;
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform $duration $easing;
}
.ml__name:hover .ml__line--name,
.ml__follow:hover .ml__line--btn,
.ml__follow:focus-visible .ml__line--btn,
.ml__follow[aria-pressed="true"] .ml__line--btn { transform: scaleX(1); }

05回転グラデーション枠

円錐グラデーションがカードの周りを一周し、内側の板がその上を覆って4pxの枠だけが光として残ります。角度を持つカスタムプロパティを @property で登録することが、この項目のすべてです。

@propertyconic-gradient--angle
@property --angle {
  syntax: "<angle>";
  initial-value: 0deg;
  inherits: false;
}
.gr__rim {
  padding: $sp-1;
  background: conic-gradient(from var(--angle),
    $subject-blue, $subject-pink, $subject-mint, $subject-lilac, $subject-blue);
  animation-name: gr-spin;
  animation-duration: $dur-loop;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes gr-spin { to { --angle: 360deg; } }

06ダークグローリング

暗いカードの上で、アバターを囲む光のにじみと状態点が呼吸するように明るくなったり弱まったりします。箱には box-shadow を内と外に掛け、文字には輪郭をたどる drop-shadow を掛けて、にじみを使い分けます。

box-shadowdrop-shadowaria-pressed
.dg__ring {
  border: 2px solid $subject-mint;
  border-radius: 50%;
  box-shadow: 0 0 18px rgba($subject-mint, .55), inset 0 0 10px rgba($subject-mint, .45);
}
.dg__init {
  color: $subject-cream;
  filter: drop-shadow(0 0 6px rgba($subject-mint, .9));
}
@keyframes dg-breathe {
  0% { opacity: .18; transform: scale(.92); }
  38% { opacity: 1; transform: scale(1.10); }
  62% { opacity: 1; transform: scale(1.10); }
}

073Dフリップ連絡先

カードが縦軸を中心に半回転し、表の紹介が裏の連絡先に変わります。両面とも自分の裏側を隠さないと、反対の面の文字が鏡のように透けて見えます。

rotateYbackface-visibilitytransform-style
.fc { perspective: 900px; width: 300px; height: 212px; }
.fc__inner {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform $duration $easing;
}
.fc__inner.is-back { transform: rotateY(180deg); }
.fc__face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.fc__face--back { transform: rotateY(180deg); }

08ソーシャル数値カウントアップ

3つの数値が0から実際の値までフレームごとに上がります。フォローを押すとフォロワーの目標値が1増えてその数字だけ数え直し、古いカウントが上書きし続けないよう実行ごとに番号を振ります。

requestAnimationFrametabular-numsaria-live
function countTo(i, to, ms) {
  var run = ++runs[i];
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    current[i] = to;
    nums[i].textContent = commas(to);
    return;
  }
  var from = current[i], t0 = performance.now();
  function frame(now) {
    if (runs[i] !== run) return;
    var p = Math.min(1, (now - t0) / ms);
    current[i] = Math.round(from + (to - from) * easeOutCubic(p));
    nums[i].textContent = commas(current[i]);
    if (p < 1) requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}

09チームグリッド展開

タイルがひとつ広がると役職とフォローボタンが下から現れ、残りの2つが同じ時間で狭まるため、行全体の幅は変わりません。キーボードで焦点を移すときはスクロールを止めます。

flex-growtranslateYaria-expanded
mains[i].addEventListener('keydown', function (e) {
  if (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight') return;
  e.preventDefault();
  root.classList.remove('is-demo');
  var next = (i + (e.key === 'ArrowRight' ? 1 : tiles.length - 1)) % tiles.length;
  open(next);
  mains[next].focus({ preventScroll: true });
});

どこで壊れるか — 落とし穴

今回の落とし穴はスタイルではなくサムネイルで出ました。グリッドや共有カードに使う代表画像は、2秒を24枚に切り出して「直前の1枚と最も大きく変わった1枚」を選ぶのですが、07のフリップは必ず真横から見た場面が選ばれました。オレンジの背景に白い線が1本だけ残った絵です。偶然ではありません。縦軸で回った面の画面上の幅は回転角のコサインで、その変化率はサインなので、90度付近でいつも最大になります。等速でもイージング付きでも、最も変わった1枚は真横です。

同じ症状があと3つありました。08の数値3枠と09の役職・ボタンを透明度でふんわり出したところ代表画像が半透明の幽霊になり、04の下線は等速で引いたせいで開始直後の短い棒が選ばれ、書き損じのように見えました。直し方の考え方は3つとも同じで、じわじわ変わる区間をなくし、完成した状態が1枚のうちに現れるようにしました。08と09は透明度の区間を steps(1, end) の切り替えに置き換え、その切り替えが完成した状態に重なるよう合わせています。04は透明度ではなく線の伸びなので、最後のひと区切りを1枚ぶんの間隔である83.3ミリ秒に詰め込み、線が端まで引かれる1枚が最大の変化になるようにしました。07はプレビュー用のループだけを切り替えに変え、実際のクリックはなめらかな遷移のまま残しました。切り替えにしたことで回転中に画面を掃いていた面積が消え、変化したピクセルの面積は56.2%から44.0%に下がったので、代わりに透明度を使わない小さな動きを両面に入れて、動いた枚数を10枚から18枚に戻しています。切り替えだけでは動きが2枚しかなく、アニメーションなしと測定されてしまうためです。

遅延を付けたアニメーションでもうひとつ引っかかりました。animation-delay だけ付けて animation-fill-mode: backwards を省くと、遅延の間ずっと要素は書いたままの状態で描画されます。08の数値3枠は最初の1枚に全部見えていて、消えてから順番に点き直しました。遅延の間はアニメーションがまだ適用されておらず、フレームを切り出す側はその時刻をそのまま写すからです。ずらしを animation の一括指定で書くと遅延そのものが0に戻るのも同じ系統で、03・04・06・07・08・09はすべて個別プロパティで書いています。

アクセシビリティ

色のコントラストは推測せず、合成した実際の色で計算しました。半透明の板の下では、背景と混ざったあとの色が本当の背景になるため、目には白い文字がはっきり見えても数値は合格になりません。

項目 文字 / 背景 判定
01 ガラス 墨色 / 曇り板 (255, 130, 98) 7.49:1 合格
01 ガラス(捨てた案) 白 / 同じ曇り板 2.44:1 不足
02 ネオブルータリズム 白 / 青のピル 4.53:1 合格
03 クレー 墨色62% / ピンクのカード 4.79:1 合格
04 ミニマル 墨色64% / 白いカード 5.44:1 合格
06 ダーク ミントの状態点 / 暗い板 8.64:1 合格
09 チーム クリーム / 広がったタイル 10.99:1 合格

01は当初白い文字で、2.44:1と基準に届きませんでした。曇り板をより白くして文字を墨色に変えたところ7.49:1になりました。04の補助文字も不透明度55%で4.04:1だったため64%に上げています。コントラストまで直した9組のソースは、圧縮パスワード kx4qjk9q を入れると開くzipの中にあり、バニラ版とReact版が同じ値を使っています。

prefers-reduced-motion: reduce では9つともCSSの自動再生ループを止めます。01のにじみは最初の位置のまま動かず、02は繰り返しの代わりに実際の :active だけが残り、03の呼吸はふくらむ前の大きさで止まり、05の枠は回転を止めてもグラデーションはそのまま見えます。06は脈打つ光だけが弱い側で止まり、固定の box-shadow によるにじみは残ります。07は反転の遷移が切れてクリックすると即座に裏面になり、08は数値の枠が順に現れる演出と進行バーが消え、09は広がる遷移なしで状態だけ変わります。08の数値のカウントアップだけはCSSではなくJavaScriptで書かれていてメディアクエリが自動では届かないため、バニラ版とReact版の両方で matchMedia('(prefers-reduced-motion: reduce)') を一度読み、数えずに最終値をそのまま入れています。どの場合もフォロー済みかどうか、広がっているかどうかという状態の値は残ります。07の3D関連のプロパティは MDNのtransform-styleの解説にまとまっています。

同じカード系をもっと探すなら、CSS カード ホバー 効果9選ログイン画面 デザイン 9選が近いテーマです。

FAQ

アバター画像なしでイニシャルだけでも大丈夫ですか

大丈夫です。9つとも画像ファイルを使わず、CSSのグラデーションで塗った円の中にイニシャル2文字を入れています。写真がまだないアカウントでは、灰色の人型よりも空きが目立たず、ネットワークリクエストも1件も増えません。あとで写真が用意できたら円の内側を background-image で覆うだけで済み、外側の大きさや枠はそのままにできます。

backdrop-filter を入れたのに何もぼけません

その要素の後ろにぼける対象がないからです。backdrop-filter は要素自身ではなく後ろに敷かれたものをぼかすので、単色の背景に重ねるとぼけた単色になり、違いがまったく見えません。01のにじみや写真のように、後ろに何かが必要です。カードの背景が完全に不透明でも同じように結果が隠れるので、アルファ値は必ず1未満にします。

グラデーションの枠が回りません

@property の登録が抜けている可能性が高いです。ただ宣言しただけのカスタムプロパティはブラウザが文字列として扱うため2つの値の間を補間できず、角度が0度から360度へ一気に跳んで止まって見えます。05のように syntax: "<angle>"initial-value を書いて登録すると、角度が実際につながって動きます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。