GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

ポートフォリオサイト テンプレート9選 — プロジェクト紹介セクション

ポートフォリオサイト テンプレートは、紹介ヒーロー・プロジェクトグリッド・ケース詳細・経歴タイムライン・連絡フォームなど、個人のポートフォリオページに使うセクションをまとめて配るコード集です。

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

9つは、採用担当者やクライアントがポートフォリオサイトを開いてから読み進める順番に並べました。まず何者かを伝え(紹介ヒーロー)、手がけた仕事を分類して見せ(プロジェクトグリッド)、代表的な1件を数字つきで深掘りし(ケース詳細ブロック)、扱える技術を並べ(技術スタック)、歩んできた経歴を示し(経歴タイムライン)、第三者の言葉で裏付けを足し(推薦の声)、連絡先を渡し(連絡フォーム)、履歴書を持ち帰らせ(履歴書CTA)、最後に他のリンクへ導きます(フッター)。顔写真や作品カットが入る場所に置いたのは実物ではなく、$subject-ink・$subject-blue・$subject-creamの3色だけで組んだ図形です。差し込むファイルを入れ替えるだけで使える ポートフォリオサイト テンプレート html なので、周りの余白や整列はそのまま残ります。

01紹介ヒーロー

3つの円が別々の軌道で

背景でぼかした円3つが、それぞれ違う軌道で絶えず漂い続けます。

紹介ヒーロー
新しいタブで開く

01 · 紹介ヒーロー — 計測 累積面積 31.004% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transformopacityaria-pressed

ボタンは実際に切り替わる

その手前に図形のワードマーク・見出し・サブ見出し・CTAボタン2つが1画面に収まり、右側の「新しい案件の相談を受け付ける」ボタンは押すたびに状態とアイコンの塗りが実際に切り替わります。

.ih__orb--1 { animation: ih-float-a 2s ease-in-out infinite; }
@keyframes ih-float-a {
  0%, 100% { transform: translate(0, 0) scale(1); }
  50%      { transform: translate($sp-10, -$sp-8) scale(1.5); }
}
.ih__avail-btn[aria-pressed="true"] {
  background: rgba(255, 247, 230, .12);
  border-color: $subject-cream;
}
.ih__avail-btn[aria-pressed="true"] .ih__avail-ic {
  opacity: 1;
  color: $color;
  transform: scale(1.2);
}

02プロジェクトグリッド

ポートフォリオ ギャラリーをタブで絞る

上部の分類タブ(すべて・Web・アプリ・デザイン)をクリックすると、黒いインジケーターがそのタブの実際の幅・位置まで移動し、選ばれていない分類のカードは実際に薄くなります。

プロジェクトグリッド
新しいタブで開く

02 · プロジェクトグリッド — 計測 累積面積 14.028% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

getBoundingClientRecttranslateXclick

選ばれていないカードは薄くなる

インジケーターの位置は決め打ちのpxではなく、クリックのたびにgetBoundingClientRectで測り直した値です。

function moveIndicator(tab) {
  var wrapBox = tab.parentElement.getBoundingClientRect();
  var box = tab.getBoundingClientRect();
  indicator.style.width = box.width + 'px';
  indicator.style.transform = 'translateX(' + (box.left - wrapBox.left) + 'px)';
}
tabs.forEach(function (tab) {
  tab.addEventListener('click', function () {
    root.classList.remove('is-demo');
    tabs.forEach(function (t) { t.setAttribute('aria-selected', 'false'); });
    tab.setAttribute('aria-selected', 'true');
    moveIndicator(tab);
    var cat = tab.getAttribute('data-cat');
    cards.forEach(function (card) {
      var show = cat === 'all' || card.getAttribute('data-cat') === cat;
      card.classList.toggle('is-hidden', !show);
    });
  });
});

03ケース詳細ブロック

パネルが開いて数字が上がる

「結果を見る」を押すとパネルが実際に開き、転換率・制作期間・トラフィック増加の3つの数字が0から目標値まで上がっていきます。

ケース詳細
新しいタブで開く

03 · ケース詳細 — 計測 累積面積 0.461% · 動いたフレーム 17/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

requestAnimationFramemax-heightclick

アクセントバーは常に動く

カード上部のアクセントバーはクリックの有無にかかわらず、常にスキャンする動きを繰り返します。

function countUp() {
  if (hasCounted) return;
  hasCounted = true;
  var startTime = performance.now();
  var duration = 900;
  function easeOut(t) { return 1 - Math.pow(1 - t, 3); }
  function tick(now) {
    var t = Math.min(1, (now - startTime) / duration);
    var e = easeOut(t);
    nums.forEach(function (el) {
      var target = Number(el.getAttribute('data-target'));
      var suffix = el.getAttribute('data-suffix');
      el.textContent = Math.round(target * e) + suffix;
    });
    if (t < 1) requestAnimationFrame(tick);
  }
  requestAnimationFrame(tick);
}

04技術スタック

8個のチップが流れ続ける

図形で組んだ技術チップ8個(実物のロゴではありません)を並べたトラックが、継ぎ目を感じさせずに流れ続けます。

技術スタック
新しいタブで開く

04 · 技術スタック — 計測 累積面積 8.058% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

translateX(-50%)animation-play-statepointerenter

その場で止める

カーソルを乗せるか横のボタンを押している間だけ、その場で流れが止まります。

// トラックそのものには gap を使わない — 項目ごとに margin-right で間隔を確保する
.sk__track { display: flex; width: max-content; animation: sk-scroll $duration $easing infinite; }
.sk.is-paused .sk__track,
.sk__viewport:hover .sk__track { animation-play-state: paused; }
@keyframes sk-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
.sk__chip {
  flex: 0 0 auto;
  width: 68px; height: 32px;
  margin-right: $sp-5;
  border-radius: $r-sm;
  background: rgba(23, 20, 26, .06);
}

05経歴タイムライン

進捗線が実際に伸びる

縦線上に並んだ経歴の4地点をクリックすると、その地点までの進捗線がscaleYで実際に伸びて塗りつぶされます。

経歴タイムライン
新しいタブで開く

05 · 経歴タイムライン — 計測 累積面積 0.51% · 動いたフレーム 8/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleYclickaria-current

説明文も切り替わる

上の説明文もその時点の内容に切り替わります。

var careerNotes = [
  '2023年、プロジェクトAを最初から最後までリードしました。',
  '2022年、プロジェクトBにフロントエンド担当として参加しました。',
  '2021年、プロジェクトCチームでインターンとしてUIを実装しました。',
  '2020年、専攻を修了し、初めてのサイドプロジェクトを始めました。'
];
var root = document.querySelector('.et');
var stops = root.querySelectorAll('.et__stop');
var desc = root.querySelector('.et__desc');
stops.forEach(function (btn) {
  btn.addEventListener('click', function () {
    root.classList.remove('is-demo');
    var i = Number(btn.getAttribute('data-i'));
    root.setAttribute('data-active', String(i));
    root.querySelector('.et__fill').style.transform = 'scaleY(' + ((i + 1) / stops.length) + ')';
    stops.forEach(function (s) { s.removeAttribute('aria-current'); });
    btn.setAttribute('aria-current', 'step');
    desc.textContent = careerNotes[i];
  });
});

06推薦の声

3枚を角度をずらして重ねる

推薦文のカード3枚が、少しずつ角度をずらして重なっています。

推薦の声
新しいタブで開く

06 · 推薦の声 — 計測 累積面積 0.188% · 動いたフレーム 16/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

transformclickaria-live

一番手前が一番奥へ

「次の推薦文」をクリックすると一番手前のカードが実際に一番奥へ回り込み、束の並び順そのものが入れ替わります。

function render() {
  cards.forEach(function (card, i) { card.setAttribute('data-order', String(i)); });
  status.textContent = cards[0].querySelector('.pq__who').textContent + ' のレビューを表示中';
}
next.addEventListener('click', function () {
  root.classList.remove('is-demo');
  cards.push(cards.shift());
  render();
});

07連絡フォーム

送信前に値を検査する

名前・メールアドレスを入力して送信すると、実際に値を検査します。

連絡フォーム
新しいタブで開く

07 · 連絡フォーム — 計測 累積面積 0.917% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

focusstroke-dashoffsetsubmit

空欄が知らせる方法

空欄のままの項目は短く左右に揺れながらフォーカスを受け取り、両方通ると送信ボタンにチェックマークが描かれます。

function shakeField(el) {
  el.classList.remove('is-shake');
  void el.offsetWidth;
  el.classList.add('is-shake');
}
form.addEventListener('submit', function (e) {
  e.preventDefault();
  var hasName = name.value.trim().length > 0;
  var isEmailValid = /^\S+@\S+\.\S+$/.test(email.value);
  if (!hasName) {
    shakeField(name);
    name.focus({ preventScroll: true });
    status.textContent = 'お名前を入力してください。';
    return;
  }
  if (!isEmailValid) {
    shakeField(email);
    email.focus({ preventScroll: true });
    status.textContent = '正しいメールアドレスを入力してください。';
    return;
  }
  form.classList.add('is-done');
  status.textContent = 'メッセージを送信しました。';
});

08履歴書CTA

進行バーが満タンになると

「PDFをダウンロード」を押すと、ボタン内の進行バーが実際に満タンまで伸びたあと矢印アイコンがチェックマークに切り替わり、しばらくすると元の見た目に戻ります。

履歴書CTA
新しいタブで開く

08 · 履歴書CTA — 計測 累積面積 0.111% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

scaleXsetTimeoutstroke-dashoffset

押す前は矢印が跳ねる

押す前は矢印だけが小さく上下に跳ねています。

btn.addEventListener('click', function () {
  if (isBusy) return;
  isBusy = true;
  btn.classList.add('is-loading');
  status.textContent = '準備中';
  setTimeout(function () {
    btn.classList.remove('is-loading');
    btn.classList.add('is-done');
    status.textContent = 'ダウンロード完了';
    setTimeout(function () {
      btn.classList.remove('is-done');
      status.textContent = '';
      isBusy = false;
    }, 1600);
  }, 900);
});

09ショーケースフッター

正規表現で書式を判定

メール欄にアドレスを入れて購読を押すと、正規表現で実際に書式を判定します。正しければチェックマークが描かれて完了文が表示され、誤っていれば入力欄が短く左右に震えます。

フッター
新しいタブで開く

09 · フッター — 計測 累積面積 0.583% · 動いたフレーム 7/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

submitstroke-dashoffsetclick

下の3列は動かない

下にはショーケース・連絡・フォローの3列のリンクが並びます。

form.addEventListener('submit', function (e) {
  e.preventDefault();
  var ok = /^\S+@\S+\.\S+$/.test(input.value);
  if (ok) {
    form.classList.add('is-done');
    status.textContent = '登録完了 — 新しいプロジェクトをメールでお届けします。';
  } else {
    input.classList.remove('is-shake');
    void input.offsetWidth;
    input.classList.add('is-shake');
    status.textContent = '正しいメールアドレスを入力してください。';
  }
});

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

4回つまずいた

9つを組み立てる途中、実際に4回つまずきました。

サムネイルがはみ出した

ひとつ目はプロジェクトグリッド(02)です。サムネイルの高さを44pxのままにしてタブ周りの余白も広く取っていたところ、カード2段とタブ行を足した縦の長さが480×300のキャンバスを超えて、レンダリングした画面にスクロールバーが出てしまいました。サムネイルを30pxまで縮め、タブの余白とグリッドの隙間を詰めることで1画面に収まりました。

薄い装飾は測れない

ふたつ目はケース詳細ブロック(03)です。最初は「結果を見る」ボタンの周りに細いリング状の縁を薄く点滅させる装飾を入れていましたが、フレーム間の変化量が小さすぎて、23枚中わずか4枚でしか動きとして拾われませんでした。カード上部に太めのアクセントバーを置いてscaleXで大きくスキャンさせる方式に切り替えると、17枚まで一気に増えました。色の差が薄い装飾は、実測値もそのぶん小さく出ます。

箱がカードより小さかった

みっつ目は推薦の声(06)です。カードの重なりを収める箱の高さを128pxに決めていましたが、実際のカード内容(大きな引用符・2行の文章・氏名・内側の余白)のほうが大きく、後ろのカードの文字が前のカードの枠をはみ出して「次の推薦文」ボタンと重なって見えていました。箱の高さを164pxに広げ、引用符のサイズを縮めたところ重なりは解消しました。

装飾がないと0になる

よっつ目は連絡フォーム(07)です。この項目は動きがすべて送信イベントに紐づいていたため、最初は常時再生する装飾が1つもなく、自動レンダリングの計測でアニメーションがまったく検出されませんでした。ほかの項目にならって送信ボタンにうっすらとしたグローの点滅を足したところ、ようやく動きが測定されました。上の4つを直したあとのコードは、pcbj2jd9で開くzipの中身とそのまま一致しています。

アクセシビリティ

装飾だけ止めて状態は残す

モーションを減らす設定(prefers-reduced-motion: reduce)のブラウザでは、9つとも装飾のための動きだけが止まり、そのときの状態は保たれます — 経歴の進捗線は伸びきったまま、プロジェクトグリッドは絞り込んだ状態のまま固定され、ケース詳細のカウントアップの数字は情報そのものなので値を消しません。

タブと現在地の伝え方

プロジェクトグリッド(02)はrole="tablist"とaria-selectedでタブの状態を伝え、経歴タイムライン(05)はaria-current="step"で今見ている地点を知らせます。このMDNのElement.getBoundingClientRect()解説には、プロジェクトグリッド(02)のインジケーターがなぜ決め打ちのpxではなく実測値で動くのかがまとめられています。

status とフォーカス移動

推薦の声(06)はaria-roledescription="carousel"と、結果を読み上げるrole="status"を組み合わせて使い、連絡フォーム(07)は氏名・メールアドレスが空のときfocus({ preventScroll: true })でその入力欄へフォーカスだけを移し、画面のスクロール位置はそのままにします。ランディングページ向けの他のセクションテンプレートはテンプレートカテゴリーに、このデモを作った人の情報は紹介ページにあります。

FAQ

この ポートフォリオ ギャラリー に実際の顔写真や作品スクリーンショットを使ってもいいですか?

もちろんです、むしろそれを前提に組んであります。ワードマーク・サムネイル・カード内の顔アイコンの位置はすべて図形のプレースホルダーなので、実物のファイルに差し替えても縦横比や余白はそのまま保たれます。図形に使っているのはdemo-tokens.scss内の$subject-ink・$subject-blue・$subject-creamという3色だけで、キャンバスの下地に敷いたイエローやオレンジは独立した別グループの変数なので、切り替えても人物・作品側の色には影響しません。

React版はCSS版と動きが違いますか?

いいえ、判定ロジックも見た目の変化も同じです。選択中のタブ・パネルの開閉・カードの並び順といった値はuseStateで保持し、変数3つ(duration・easing・color)はpropsで渡してCSS変数(--duration・--easing・--color)に橋渡しします。プロジェクトグリッド(02)のインジケーターだけはuseEffectの中でgetBoundingClientRectを呼び出し、マウント直後とクリックのたびに位置を測り直します。

連絡フォームや履歴書CTAは、実際にメールが届いたりPDFがダウンロードされたりしますか?

入力チェック・フォーカス移動・進行バー・チェックマークの描画は、どれも本物の状態変化です。ただしデモ自体には送信先のサーバーも実ファイルのPDFもつながっていないため、クリックしても本当にメールが送られたりダウンロードが始まったりはしません。実際に組み込むときは、連絡フォームの送信処理にAPI呼び出しを足し、履歴書CTAの完了タイミングに本物のhrefを差し込めば、検証や演出のロジックはそのまま流用できます。

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

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