Как создать страницу портфолио: мини-проект на HTML и CSS

В этом мини-проекте соберём страницу-портфолио на HTML и CSS — от шапки с фотографией до блока с проектами и контактов. Готовый результат можно посмотреть прямо здесь, а ниже разобраны основные приёмы вёрстки: сетки, карточки, анимации и оформление отдельных блоков.

Показать полный код страницы (можно скопировать целиком)
portfolio-demo.html
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Николай Дубук - портфолио</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200;400;600;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
  :root {
    --paper: #faf6f0;
    --ink: #1c1a17;
    --dim: #7a7468;
    --accent: #e8590c;
    --line: #e4ddd0;
  }
  * { box-sizing: border-box; }
  html { scroll-behavior: smooth; }
  body {
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: 'Manrope', 'Segoe UI', Arial, sans-serif;
  }
  a { text-decoration: none; color: inherit; }
  .wrap { max-width: 1100px; margin: 0 auto; padding: 0 28px; }
 
  /* reveal-on-scroll */
  .reveal { opacity: 0; transform: translateY(26px); transition: opacity .7s ease, transform .7s ease; }
  .reveal.in-view { opacity: 1; transform: translateY(0); }
 
  /* HERO */
  .hero { padding: 70px 28px 50px; text-align: center; }
  .avatar {
    width: 130px; height: 130px;
    border-radius: 50%;
    object-fit: cover;
    margin: 0 auto 30px;
    display: block;
    border: 1px solid var(--line);
  }
  .hero h1 {
    margin: 0;
    line-height: 1.05;
    letter-spacing: -.01em;
  }
  .hero h1 .thin {
    display: block;
    font-weight: 200;
    font-size: clamp(22px, 4vw, 34px);
    text-transform: uppercase;
    letter-spacing: .08em;
    color: var(--dim);
  }
  .hero h1 .bold {
    display: block;
    font-weight: 800;
    font-size: clamp(40px, 8vw, 76px);
    text-transform: uppercase;
  }
  .hero p {
    max-width: 480px;
    margin: 22px auto 0;
    color: var(--dim);
    font-size: 17px;
    line-height: 1.6;
  }
  .btn {
    display: inline-block;
    margin-top: 26px;
    padding: 13px 28px;
    border: 1px solid var(--ink);
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: .04em;
    text-transform: uppercase;
    transition: background .25s ease, color .25s ease;
  }
  .btn:hover { background: var(--ink); color: var(--paper); }
  .btn-accent { border-color: var(--accent); background: var(--accent); color: #fff; }
  .btn-accent:hover { background: #c94a09; color: #fff; }
 
  /* TICKER */
  .ticker {
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
    padding: 14px 0;
    overflow: hidden;
    white-space: nowrap;
  }
  .ticker-track {
    display: inline-block;
    padding-left: 100%;
    animation: scrollx 24s linear infinite;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    letter-spacing: .05em;
    color: var(--dim);
    text-transform: uppercase;
  }
  .ticker-track span { margin: 0 20px; }
  .ticker-track span.accent { color: var(--accent); }
  @keyframes scrollx { from { transform: translateX(0); } to { transform: translateX(-100%); } }
 
  /* "Я верстальщик" split section */
  .split {
    display: grid;
    grid-template-columns: 0.9fr 1.3fr;
    gap: 26px;
    align-items: center;
    padding: 70px 28px;
  }
  .split-label {
    font-size: clamp(28px, 5vw, 46px);
    font-weight: 800;
    text-transform: uppercase;
    line-height: 1.1;
  }
  .split-label span { color: var(--accent); }
  .panels { display: grid; grid-template-columns: 1.2fr 1fr; gap: 18px; }
  .code-panel {
    background: var(--ink);
    color: #f2ede4;
    border-radius: 16px;
    padding: 22px 24px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 13px;
    line-height: 1.7;
    overflow: hidden;
  }
  .code-panel .tag { color: #ff9d5c; }
  .code-panel .attr { color: #7ee787; }
  .code-panel .str { color: #9ecbff; }
  .code-panel .cm { color: #8b8478; }
  .stat-panel {
    background: var(--accent);
    color: #fff;
    border-radius: 16px;
    padding: 24px;
    display: flex;
    flex-direction: column;
    justify-content: center;
  }
  .stat-panel .num { font-size: 46px; font-weight: 800; line-height: 1; margin-bottom: 6px; }
  .stat-panel .lbl { font-size: 13px; text-transform: uppercase; letter-spacing: .05em; opacity: .9; }
 
  /* BIG BROKEN HEADING */
  .statement { padding: 70px 28px; text-align: center; }
  .statement h2 {
    font-size: clamp(38px, 9vw, 92px);
    font-weight: 800;
    text-transform: uppercase;
    line-height: .98;
    margin: 0 0 30px;
    letter-spacing: -.01em;
  }
  .statement h2 .light { font-weight: 200; }
  .statement h2 .accent { color: var(--accent); }
  .rule { width: 60px; height: 2px; background: var(--ink); margin: 0 auto 30px; }
  .statement p.quote {
    max-width: 560px;
    margin: 0 auto 34px;
    font-style: italic;
    font-size: 19px;
    color: var(--dim);
    line-height: 1.6;
  }
  .down-link { display: inline-flex; flex-direction: column; align-items: center; gap: 10px; font-size: 12px; text-transform: uppercase; letter-spacing: .1em; font-weight: 600; }
  .down-arrow { animation: bob 1.8s ease-in-out infinite; font-size: 20px; }
  @keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(6px); } }
 
  /* PROJECT GRID */
  .projects { padding: 30px 28px 90px; }
  .grid5 {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-auto-rows: 220px;
    gap: 16px;
  }
  .pcard {
    position: relative;
    border-radius: 16px;
    padding: 24px;
    color: #fff;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    overflow: hidden;
    transition: transform .35s ease;
  }
  .pcard:hover { transform: translateY(-6px); }
  .pcard.big { grid-column: span 2; }
  .pcard h3 { margin: 0 0 6px; font-size: 20px; }
  .pcard p { margin: 0 0 14px; font-size: 13px; opacity: .88; line-height: 1.5; max-width: 320px; }
  .pcard .go { font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; display: inline-flex; align-items: center; gap: 6px; }
  .pcard .go .arrow { transition: transform .3s ease; }
  .pcard:hover .go .arrow { transform: translateX(5px); }
  .p1 { background: linear-gradient(135deg,#1c1a17,#3a352c); }
  .p2 { background: linear-gradient(135deg,#e8590c,#ff8a3d); }
  .p3 { background: linear-gradient(135deg,#2d5c4a,#4c9376); }
  .p4 { background: linear-gradient(135deg,#5b4a8a,#8a6fd6); }
  .p5 { background: linear-gradient(135deg,#8a4a3a,#c97a55); }
 
  /* ABOUT TWO COL */
  .about { padding: 70px 28px; border-top: 1px solid var(--line); }
  .about-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 50px; }
  .about h3 { font-size: 26px; margin: 0 0 16px; font-weight: 800; }
  .about p { color: var(--dim); line-height: 1.65; font-size: 15px; margin: 0 0 18px; }
  .values { display: flex; gap: 22px; margin-top: 16px; flex-wrap: wrap; }
  .values span { font-size: 12px; text-transform: uppercase; letter-spacing: .06em; font-weight: 700; padding: 8px 14px; border: 1px solid var(--line); border-radius: 999px; color: var(--ink); }
 
  /* FOOTER */
  footer { background: var(--ink); color: #f2ede4; padding: 60px 28px; text-align: center; }
  footer p { color: #b5ab99; margin: 0 0 26px; }
  .social { display: flex; justify-content: center; gap: 16px; flex-wrap: wrap; }
  .social a {
    padding: 11px 26px;
    border: 1px solid #4a453d;
    border-radius: 999px;
    font-size: 13px;
    font-weight: 600;
    transition: background .25s ease, border-color .25s ease;
  }
  .social a:hover { background: var(--accent); border-color: var(--accent); }
 
  @media (prefers-reduced-motion: reduce) {
    * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
  }
 
  @media (max-width: 760px) {
    .split { grid-template-columns: 1fr; text-align: center; }
    .panels { grid-template-columns: 1fr; }
    .grid5 { grid-template-columns: 1fr; grid-auto-rows: 200px; }
    .pcard.big { grid-column: span 1; }
    .about-grid { grid-template-columns: 1fr; gap: 30px; }
  }
</style>
</head>
<body>
 
<header class="hero">
  <img class="avatar" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2ygCw8MfZ-AsLyuyAtW9hBXID12ad1pYFPt1h_MnXM-oa_1MsIdKrPUOFOm4uRUKuNFq219L-YZboPcr8jGkxqO2H2eZJNb8m3fc2e-o5-9ETqfrSsV2SSy8JXMkuLHKd_YqIVcqEdpjiV55i2NwXtDFAd7WKVSBzgY1duHPMtwDHB80q-AQ4Zqrhygg/s1600-rw/Nikolai_Dubuk_2.jpg" alt="Николай Дубук">
  <h1><span class="thin">Автор курса</span><span class="bold">HTML &amp; CSS</span></h1>
  <p>Веду блоги на Blogger уже 11 лет. Учу вёрстке бесплатно - потому что бесплатных курсов по HTML и CSS на русском на удивление мало.</p>
  <a class="btn" href="https://www.htmlicss.com/p/obo-mne.html" target="_blank" rel="noopener">Обо мне</a>
</header>
 
<div class="ticker">
  <div class="ticker-track">
    <span class="accent">HTML</span><span>·</span><span class="accent">CSS</span><span>·</span><span>GRID</span><span>·</span><span>FLEXBOX</span><span>·</span><span class="accent">BLOGGER</span><span>·</span><span>11 ЛЕТ В ДЕЛЕ</span><span>·</span>
    <span class="accent">HTML</span><span>·</span><span class="accent">CSS</span><span>·</span><span>GRID</span><span>·</span><span>FLEXBOX</span><span>·</span><span class="accent">BLOGGER</span><span>·</span><span>11 ЛЕТ В ДЕЛЕ</span><span>·</span>
  </div>
</div>
 
<section class="split reveal">
  <div class="split-label">Я <span>верстальщик</span></div>
  <div class="panels">
    <div class="code-panel">
      <span class="cm">/* портфолио на HTML и CSS */</span><br>
      <span class="tag">&lt;section</span> <span class="attr">class</span>=<span class="str">"projects"</span><span class="tag">&gt;</span><br>
      &nbsp;&nbsp;<span class="tag">&lt;article&gt;</span>...<span class="tag">&lt;/article&gt;</span><br>
      <span class="tag">&lt;/section&gt;</span><br><br>
      <span class="cm">.projects {</span><br>
      &nbsp;&nbsp;display: grid;<br>
      &nbsp;&nbsp;gap: 16px;<br>
      <span class="cm">}</span>
    </div>
    <div class="stat-panel">
      <div class="num">5</div>
      <div class="lbl">своих проектов на Blogger</div>
    </div>
  </div>
</section>
 
<section class="statement reveal">
  <h2><span class="light">Пишу</span> код<br><span class="accent">без команды</span><br><span class="light">и без</span> агентства</h2>
  <div class="rule"></div>
  <p class="quote">Я не делаю сайты ради денег - довожу проект до конца, чтобы доказать себе, что могу.</p>
  <a class="down-link" href="#proekty">
    <span class="down-arrow">&darr;</span>
    Смотреть проекты
  </a>
</section>
 
<section class="projects" id="proekty">
  <div class="grid5 reveal">
    <article class="pcard p1 big">
      <h3>HTML&amp;CSS - учимся вёрстке</h3>
      <p>Бесплатный курс HTML и CSS с интерактивными примерами прямо в уроке.</p>
      <a class="go" href="https://www.htmlicss.com/" target="_blank" rel="noopener">Открыть <span class="arrow">&rarr;</span></a>
    </article>
    <article class="pcard p2">
      <h3>LeV Template</h3>
      <p>Каталог шаблонов Blogger.</p>
      <a class="go" href="https://lev-template.com/" target="_blank" rel="noopener">Открыть <span class="arrow">&rarr;</span></a>
    </article>
    <article class="pcard p3">
      <h3>Путь блогера</h3>
      <p>Гид по Blogger для новичков.</p>
      <a class="go" href="https://bloggers-put.com/" target="_blank" rel="noopener">Открыть <span class="arrow">&rarr;</span></a>
    </article>
    <article class="pcard p4">
      <h3>ВАЗ-2101 - Электрооборудование</h3>
      <p>Блог об электрооборудовании ВАЗ-2101.</p>
      <a class="go" href="https://vaz01.blogspot.com/" target="_blank" rel="noopener">Открыть <span class="arrow">&rarr;</span></a>
    </article>
    <article class="pcard p5 big">
      <h3>Азбука пчеловода</h3>
      <p>Сайт о пчеловодстве: биографические статьи и обзоры книг.</p>
      <a class="go" href="https://www.azbuka-pchelovoda.com/" target="_blank" rel="noopener">Открыть <span class="arrow">&rarr;</span></a>
    </article>
  </div>
</section>
 
<section class="about reveal">
  <div class="wrap about-grid">
    <div>
      <h3>Качество, честность, помощь</h3>
      <p>Три принципа, на которых держатся все проекты выше - без команды, без агентства, без спонсоров.</p>
      <div class="values">
        <span>Качество</span>
        <span>Честность</span>
        <span>Помощь</span>
      </div>
    </div>
    <div>
      <h3>Как со мной связаться</h3>
      <p>Быстрее всего отвечаю в Telegram. Есть вопрос по уроку или сайту - пишите, разберём.</p>
      <a class="btn btn-accent" href="https://t.me/nikolaidubuk" target="_blank" rel="noopener">Написать в Telegram</a>
    </div>
  </div>
</section>
 
<footer>
  <p>Николай Дубук - вёрстка, блоги, Blogger</p>
  <div class="social">
    <a href="https://t.me/nikolaidubuk" target="_blank" rel="noopener">Telegram</a>
    <a href="mailto:bloggers.put@gmail.com">Email</a>
    <a href="https://www.htmlicss.com/p/kontakti.html" target="_blank" rel="noopener">Все контакты</a>
  </div>
</footer>
 
<script>
  (function () {
    var els = document.querySelectorAll('.reveal');
    if (!('IntersectionObserver' in window)) {
      els.forEach(function (el) { el.classList.add('in-view'); });
      return;
    }
    var io = new IntersectionObserver(function (entries) {
      entries.forEach(function (entry) {
        if (entry.isIntersecting) {
          entry.target.classList.add('in-view');
          io.unobserve(entry.target);
        }
      });
    }, { threshold: 0.12 });
    els.forEach(function (el) { io.observe(el); });
  })();
 
  // manual scroll for in-page anchors - needed when this page runs inside
  // a srcdoc iframe, where the inherited parent base URL can stop the
  // default anchor-jump behaviour from working
  (function () {
    document.querySelectorAll('a[href^="#"]').forEach(function (a) {
      a.addEventListener('click', function (e) {
        var id = a.getAttribute('href').slice(1);
        var target = document.getElementById(id);
        if (target) {
          e.preventDefault();
          target.scrollIntoView({ behavior: 'smooth' });
        }
      });
    });
  })();
</script>
 
</body>
</html>

Шапка: фото и контраст шрифтов

Фото превращено в круг одним свойством border-radius: 50%. Под ним - имя в два уровня: тонкая надпись "Автор курса" (font-weight: 200) и жирный заголовок "HTML & CSS" (font-weight: 800) одним и тем же шрифтом - контраст толщины вместо контраста разных шрифтов.

HTML: что выводит эта разметка

Внутри <header> - фото, заголовок из двух вложенных <span> (один со стилем "тонко", другой "жирно"), абзац с текстом и ссылка-кнопка. Никакой магии в разметке нет - весь эффект дальше делает CSS.

HTML
<header class="hero">
  <img class="avatar" src="..." alt="Николай Дубук">
  <h1>
    <span class="thin">Автор курса</span>
    <span class="bold">HTML &amp; CSS</span>
  </h1>
  <p>Веду блоги на Blogger уже 11 лет...</p>
  <a class="btn" href="...">Обо мне</a>
</header>

Подробнее: Семантическая вёрстка: header, main, footer, Изображения: тег img и атрибут alt и Ссылки: тег a и его атрибуты.

CSS: как это оформлено

CSS
.avatar {
  width: 130px;
  height: 130px;
  border-radius: 50%;
  object-fit: cover;
}
.hero h1 .thin { font-weight: 200; }
.hero h1 .bold { font-weight: 800; }

Подробнее: Скругление углов: border-radius и Насыщенность шрифта: font-weight.

Бегущая строка снизу от шапки

Строка со словами HTML / CSS / GRID и так далее едет справа налево бесконечно. Приём простой: весь текст лежит в одном блоке с display: inline-block, а анимация через @keyframes сдвигает его свойством transform: translateX() от 0 до -100% и повторяется по кругу (infinite).

HTML: что выводит эта разметка

Внутри .ticker-track список слов записан не один раз, а два одинаковых блока подряд. Это нужно для бесшовной петли: пока первый набор слов уезжает влево, на его месте уже показывается второй - глаз не замечает момента, когда анимация начинается заново.

HTML
<div class="ticker">
  <div class="ticker-track">
    <span class="accent">HTML</span>
    <span>·</span>
    <span class="accent">CSS</span>
    ...
    <!-- весь набор слов повторён ДВАЖДЫ подряд -->
  </div>
</div>

Подробнее: Теги, атрибуты и вложенность.

CSS: как это оформлено

CSS
.ticker-track {
  display: inline-block;
  padding-left: 100%;
  animation: scrollx 24s linear infinite;
}
@keyframes scrollx {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}

Подробнее: Перемещение элементов: translate, translateX, translateY и Правило @keyframes: создаём анимацию с нуля.

Сетки страницы и крупные плашки проектов

Блок "Я верстальщик" и блок "Как со мной связаться" построены на display: grid с двумя колонками. В сетке проектов у части карточек указано grid-column: span 2 - такая карточка занимает сразу две колонки вместо одной, отсюда и разные по размеру плашки вместо ровной таблицы одинаковых квадратов.

HTML: что выводит эта разметка

Это обычные <section> и внутри них ровный список одинаковых <article class="pcard"> - никакой разницы в разметке между "большой" карточкой и обычной нет, они выглядят по-разному только благодаря CSS-классам p1-p5 и big.

HTML
<section class="split">
  <div class="split-label">Я верстальщик</div>
  <div class="panels">...</div>
</section>
 
<section class="projects">
  <div class="grid5">
    <article class="pcard p1 big">...</article>
    <article class="pcard p2">...</article>
    <article class="pcard p3">...</article>
    <article class="pcard p4">...</article>
    <article class="pcard p5 big">...</article>
  </div>
</section>

Подробнее: nav, article, section, aside.

CSS: как это оформлено

CSS
.split {
  display: grid;
  grid-template-columns: 0.9fr 1.3fr;
  gap: 26px;
}
.grid5 {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 16px;
}
.pcard.big { grid-column: span 2; }

Подробнее: Что такое CSS Grid и зачем он нужен, Создание сетки: grid-template-columns и grid-template-rows и Позиционирование в сетке: grid-column, grid-row и grid-area.

При наведении курсора карточка проекта приподнимается - это обычный transition на свойстве transform, который срабатывает при переходе в состояние :hover. Ничего не пересчитывается заново - браузер просто плавно анимирует переход между "было" и "стало".

HTML: что выводит эта разметка

Карточка - это <article class="pcard"> с заголовком, описанием и ссылкой-стрелкой внутри. В самой разметке нет ни слова про наведение - состояние :hover ниже добавляет CSS, а не HTML.

HTML
<article class="pcard p1 big">
  <h3>HTML&amp;CSS - учимся вёрстке</h3>
  <p>Бесплатный курс HTML и CSS...</p>
  <a class="go" href="...">Открыть <span class="arrow">&rarr;</span></a>
</article>

Подробнее: nav, article, section, aside - тот же тег article, что и в сетке проектов выше.

CSS: как это оформлено

CSS
.pcard {
  transition: transform .35s ease;
}
.pcard:hover {
  transform: translateY(-6px);
}

Подробнее: Анимация при наведении: практика с transition и :hover и Анимированные кнопки и ссылки: практика.

Появление блоков при прокрутке

Блоки страницы изначально прозрачные и сдвинутые вниз (opacity: 0), а когда блок появляется в зоне видимости при скролле, ему через JavaScript (IntersectionObserver) добавляется класс, который CSS-переходом плавно возвращает его на место. У страницы также есть правило @media (prefers-reduced-motion: reduce) - оно отключает все анимации для тех, кто в настройках операционной системы попросил браузер их не показывать.

HTML: что выводит эта разметка

К нужным секциям просто добавлен ещё один класс - reveal - рядом с их обычным классом. Сам по себе он ничего не делает: это просто "флажок", за который цепляется и CSS (прячет блок), и JavaScript (следит за прокруткой и снимает флажок в нужный момент).

HTML
<section class="split reveal">...</section>
<section class="statement reveal">...</section>
<div class="grid5 reveal">...</div>
<section class="about reveal">...</section>

Подробнее: Теги, атрибуты и вложенность - class здесь обычный атрибут, просто использованный как переключатель для CSS и JS.

CSS + JS: как это оформлено

CSS + JS
.reveal {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .7s ease, transform .7s ease;
}
.reveal.in-view {
  opacity: 1;
  transform: translateY(0);
}
 
var io = new IntersectionObserver(function (entries) {
  entries.forEach(function (entry) {
    if (entry.isIntersecting) {
      entry.target.classList.add('in-view');
      io.unobserve(entry.target);
    }
  });
}, { threshold: 0.12 });
 
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; }
}

Подробнее: Анимация появления элементов при загрузке страницы и prefers-reduced-motion: уважение к выбору пользователя.

Категория
Поделиться в социальных сетях

Отправить комментарий

0 Комментарии

Отправить комментарий (0)

#buttons=(Хорошо) #days=(20)

Мы используем файлы cookie для улучшения работы сайта. Подробнее
Хорошо
×