GODRICH
#css-animation#button#loader#transition#scroll#ui#xu-huong-thiet-ke

9 mẫu portfolio grid css — dán vào landing

Portfolio grid css là bộ code gom sẵn từng khối của trang portfolio cá nhân: lưới dự án, case study, kinh nghiệm, form liên hệ.

Tự chạy · bấm ô để tới mục đó · cả 9 trong một zip

Chín mục xếp theo trình tự khách ghé thăm lướt qua một portfolio. Đầu tiên cho thấy bạn là ai (hero), rồi tác phẩm chia theo nhóm (lưới dự án), một tác phẩm soi sâu (case study), kỹ năng làm chủ được (stack kỹ năng), hành trình nghề nghiệp (timeline kinh nghiệm), lời người khác nói thêm niềm tin (trích dẫn đề cử), cách liên hệ (form liên hệ), nhận CV về máy (CTA CV), và cuối cùng chỉ chỗ xem thêm (footer showcase). vị trí ô ảnh chân dung·ảnh chụp dự án là placeholder hình khối vẽ bằng ba token mực·xanh·kem của site; chỉ cần thay file là bố cục giữ nguyên.

01Hero giới thiệu

Bố cục của hero section

Ba vòng tròn nền bay liên tục theo ba quỹ đạo khác nhau, phía trên wordmark (placeholder hình khối)·tiêu đề lớn·phụ đề·hai nút CTA đứng gọn không đè lên nhau.

Hero giới thiệu
Mở trong tab mới

01 · Hero giới thiệu — Diện tích đo được 31.004% · khung hình chuyển động 18/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transformopacityaria-pressed

Nút nhận đề nghị là thật

Mỗi lần nhấn nút nhận dự án mới bên phải, trạng thái và độ tô đặc của biểu tượng đổi thật.

.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);
}

02Lưới dự án

Danh sách thẻ của portfolio grid html css lọc theo nhóm

Nhấp tab phân loại phía trên (tất cả·web·app·thiết kế) là indicator màu mực dịch đúng độ rộng·vị trí thật của tab đó, còn thẻ thuộc nhóm khác mờ đi.

Lưới dự án
Mở trong tab mới

02 · Lưới dự án — Diện tích đo được 14.028% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

getBoundingClientRecttranslateXclick

Indicator chạy bằng số đo thật

Vị trí indicator không phải px phỏng đoán mà là giá trị getBoundingClientRect đo lại ở mỗi lần nhấp.

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);
    });
  });
});

03Khối case study

Bảng kết quả và bộ đếm

Nhấn xem kết quả là bảng mở ra thật, cùng ba con số tăng trưởng·thời gian làm·lưu lượng tăng từ 0 lên giá trị đích.

Khối case study
Mở trong tab mới

03 · Khối case study — Diện tích đo được 0.461% · khung hình chuyển động 17/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

requestAnimationFramemax-heightclick

Thanh accent quay không ngừng

Thanh accent trên thẻ cứ quét đi quét lại bất kể bạn đã nhấn hay chưa.

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);
}

04Stack kỹ năng

Tám chip chạy không đứt quãng

Tám chip kỹ năng vẽ bằng hình khối (không phải logo thật) nối một hàng trôi đều không điểm gãy.

Chip tech stack
Mở trong tab mới

04 · Chip tech stack — Diện tích đo được 8.058% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

translateX(-50%)animation-play-statepointerenter

Rê chuột là dừng ngay tại chỗ

Đưa con trỏ lên hoặc giữ nút bên cạnh là dòng chảy đứng lại đúng chỗ.

// track không dùng gap — mỗi chip tự mang margin-right
.sk__track {
  display: flex;
  width: max-content;
  animation: sk-scroll 13s linear infinite;
}
.sk.is-paused .sk__track,
.sk__viewport:hover .sk__track { animation-play-state: paused; }
.sk__chip {
  flex: 0 0 auto;
  width: 68px; height: 32px;
  margin-right: $sp-5;
}

05Timeline kinh nghiệm

Đường tiến trình của timeline trong trang portfolio được tô dần

Bốn mốc trên trục dọc (dự án A trưởng nhóm·dự án B tham gia·dự án C thực tập·tốt nghiệp chuyên ngành) nhấp vào đâu là tiến trình tô scaleY đến đúng đấy.

Timeline kinh nghiệm
Mở trong tab mới

05 · Timeline kinh nghiệm — Diện tích đo được 0.51% · khung hình chuyển động 8/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleYclickaria-current

Đoạn mô tả cũng đổi theo

Đoạn mô tả ngay phía trên thay nội dung khớp với mốc kinh nghiệm đó.

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];
  });
});

06Trích dẫn đề cử

Ba thẻ chồng lệch nhau

Ba thẻ lời đề cử chồng lên nhau nhưng cứ lệch pha nhau một chút.

Trích dẫn phản hồi
Mở trong tab mới

06 · Trích dẫn phản hồi — Diện tích đo được 0.188% · khung hình chuyển động 16/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

transformclickaria-live

Thẻ đầu lùi về cuối

Nhấp sang nhận xét kế tiếp là thẻ đứng đầu chuyển về cuối hàng thật, thứ tự đổi hẳn, còn dấu ngoặc kép lớn đung đưa liên tục.

function render() {
  cards.forEach(function (card, i) { card.setAttribute('data-order', String(i)); });
  status.textContent = cards[0].querySelector('.pq__who').textContent + ' review now showing';
}
next.addEventListener('click', function () {
  root.classList.remove('is-demo');
  cards.push(cards.shift());
  render();
});

07Form liên hệ

Kiểm giá trị trước khi gửi

Điền tên·email rồi nhấn gửi là giá trị được kiểm tra thật.

Form liên hệ
Mở trong tab mới

07 · Form liên hệ — Diện tích đo được 0.917% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

focusstroke-dashoffsetsubmit

Ô thiếu báo bằng cách nào

Ô thiếu rung ngắn rồi nhận focus, còn khi hợp lệ thì nút gửi vẽ thêm dấu check.

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 = 'Vui lòng nhập tên.';
    return;
  }
  if (!isEmailValid) {
    shakeField(email);
    email.focus({ preventScroll: true });
    status.textContent = 'Vui lòng nhập email hợp lệ.';
    return;
  }
  form.classList.add('is-done');
  status.textContent = 'Đã gửi tin nhắn.';
});

08CTA tải CV

Thanh tiến trình chạy xong mới đổi

Nhấn tải PDF là thanh tiến trình trong nút đổ đầy thật, biểu tượng mũi tên đổi thành check, rồi một lúc sau trở về trạng thái cũ.

CTA tải CV
Mở trong tab mới

08 · CTA tải CV — Diện tích đo được 0.111% · khung hình chuyển động 23/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

scaleXsetTimeoutstroke-dashoffset

Chưa nhấp thì mũi tên nhún

Trước khi nhấp, mũi tên cứ nhún nhẹ liên tục.

btn.addEventListener('click', function () {
  if (isBusy) return;
  isBusy = true;
  btn.classList.add('is-loading');
  status.textContent = 'Đang chuẩn bị';
  setTimeout(function () {
    btn.classList.remove('is-loading');
    btn.classList.add('is-done');
    status.textContent = 'Tải xong';
    setTimeout(function () {
      btn.classList.remove('is-done');
      status.textContent = '';
      isBusy = false;
    }, 1600);
  }, 900);
});

09Footer showcase

Biểu thức chính quy phân biệt định dạng

Điền email vào ô rồi nhấn đăng ký, một biểu thức chính quy soi định dạng thật: đúng thì vẽ check kèm thông báo hoàn tất, sai thì ô rung ngang vài nhịp.

Footer showcase
Mở trong tab mới

09 · Footer showcase — Diện tích đo được 0.583% · khung hình chuyển động 7/23Nguyên tác GODRICH · SCSS và React nằm trong tệp zip · Xem mật khẩu

submitstroke-dashoffsetclick

Ba cụm liên kết phía dưới

Phía dưới là ba cụm liên kết showcase·liên hệ·theo dõi đứng cố định.

form.addEventListener('submit', function (e) {
  e.preventDefault();
  var ok = /^\S+@\S+\.\S+$/.test(input.value);
  if (ok) {
    form.classList.add('is-done');
    status.textContent = 'Đã đăng ký — dự án mới sẽ đến qua email của bạn.';
  } else {
    input.classList.remove('is-shake');
    void input.offsetWidth;
    input.classList.add('is-shake');
    status.textContent = 'Vui lòng nhập email hợp lệ.';
  }
});

Bộ showcase dễ vỡ ở đâu — bốn cái bẫy

Vướng thật bốn lần

Trong lúc làm chín mục này tôi vướng thật bốn lần. Lần thứ nhất ở lưới dự án (02): giữ thumbnail 44px và tab padding thoải mái, cộng dồn chiều cao hai hàng thẻ với hàng tab vượt canvas 480×300 nên màn hình render lộ thanh cuộn.

Thumbnail tràn khỏi canvas

Thu thumbnail về 30px và siết tab padding·khoảng cách lưới là vừa một màn không cuộn. Lần thứ hai ở khối case study (03): lúc đầu chạy pulse nhạt một vòng viền mỏng quanh nút xem kết quả, nhưng mức chênh lệch giữa các khung hình quá nhỏ nên chỉ 4 trên 23 khung hình ghi nhận được cử động.

Trang trí quá nhạt thì không đo được

Đặt một thanh accent dày trên đầu thẻ và cho quét lớn bằng scaleX thì con số vọt lên 17 khung hình. Nghĩa là trang trí có độ chênh sáng thấp thì số đo cũng thấp theo — điều mà ảnh chụp trong mấy bộ project showcase section design ít khi nói ra. Lần thứ ba ở trích dẫn đề cử (06).

Thẻ tràn khỏi hộp

Chiều cao hộp đựng chồng thẻ đặt 128px, trong khi nội dung thẻ thật (dấu ngoặc kép lớn·2 dòng chữ·tên·lề trong) vượt hơn thế nên chữ thẻ sau tràn ra ngoài viền thẻ trước, đè lên nút xem kế tiếp. Nâng hộp lên 164px và thu nhỏ dấu ngoặc kép là hết đè. Lần thứ tư ở form liên hệ (07).

Không trang trí thì đo bằng 0

Mục này mọi cử động chỉ gói trong sự kiện gửi, lúc đầu chẳng có trang trí nào chạy thường trực, kết quả khung hình render không bắt được chuyển động nào. Thêm một vệt glow pulse dịu trên nút gửi như các mục khác thì cử động mới hiện lên trên số đo. Bản code mới nhất đã sửa cả bốn cái bẫy nằm nguyên trong zip mở bằng mật khẩu pcbj2jd9.

Khả năng tiếp cận

Chỉ tắt trang trí, giữ lại trạng thái

Trên trình duyệt đặt giảm chuyển động (prefers-reduced-motion: reduce), cả chín mục chỉ tắt phần trang trí còn trạng thái kết quả vẫn hiện nguyên — đường tiến trình kinh nghiệm dừng ở mức đã tô, lưới dự án đứng ở dạng đã lọc, còn số đếm của case study là thông tin nên không xóa giá trị.

Báo trạng thái tab và bước

Lưới dự án (02) truyền trạng thái tab qua role="tablist" và aria-selected; timeline kinh nghiệm (05) báo mốc đang xem bằng aria-current="step". Trích dẫn (06) dùng chung aria-roledescription="carousel" với role="status" cho dòng kết quả, còn form liên hệ (07) khi thiếu tên·email thì chuyển focus sang ô đó bằng focus({ preventScroll: true }), giữ màn hình đứng yên mà focus vẫn dịch.

Focus chuyển tại chỗ

Tài liệu MDN Element.getBoundingClientRect() giải thích vì sao indicator của lưới dự án (02) dùng số đo thật thay cho px phỏng đoán. Còn tìm thêm template dạng landing·email thì đảo qua chuyên mục template, và muốn biết người dựng demo là ai thì xem trang giới thiệu.

FAQ — hỏi đáp nhanh

Trong gallery portfolio dùng ảnh dự án·logo thật được không?

Được, thiết kế cứ tính sẵn cho việc đó. Thay thumbnail·avatar·wordmark bằng file thật thì kích thước·canh lề giữ nguyên. Hình khối placeholder dùng đúng ba màu mực·xanh·kem định nghĩa trong demo-tokens.scss, còn nền sân khấu từng canvas (vàng·cam) là token riêng nên hai hệ màu không đụng nhau.

Bản React khác bản CSS chỗ nào?

Cách chạy chẳng khác gì nhau. Giá trị kiểu tab đang mở·bảng đã giãn·thứ tự thẻ quản lý bằng useState, ba biến (duration·easing·color) nhận qua props rồi truyền xuống biến CSS (--duration --easing --color). Indicator của lưới dự án (02) định vị lại một lần ngay sau khi mount bằng useEffect với getBoundingClientRect, và mỗi lần nhấp đo lại theo đúng cách đó.

Form liên hệ và CTA CV có gửi mail·nhận file thật không?

Phần kiểm tra·dịch focus·thanh tiến trình·dấu check đều là thay đổi trạng thái thật. Nhưng hai demo đều chưa nối server hay file thật, nên giữ nguyên hiện trạng thì mail chưa gửi đi và PDF chưa tải về. Khi dùng thật, gắn lời gọi API vào trình xử lý submit của form và nối href thật tại thời điểm tiến trình CV hoàn tất là phần logic kiểm tra·animation còn dùng lại được nguyên vẹn.

Nhập mật khẩu file nén

Mật khẩu nằm ngay trong bài viết này. Đọc tới đó bạn sẽ thấy.