Создание эффектного портфолио: Next.js, TypeScript и анимации

Полное руководство по разработке современного портфолио с нуля, включающее глубокий разбор основ используемых технологий. Вы освоите связку Next.js и TypeScript, научитесь создавать стильные интерфейсы с Tailwind CSS и внедрять сложные анимации с Framer Motion и Lenis.

Фундамент стека: Глубокий разбор TypeScript, React и архитектуры Next.js

Фундамент стека: Глубокий разбор TypeScript, React и архитектуры Next.js

Добро пожаловать в первый модуль курса «Создание эффектного портфолио». Прежде чем мы начнем создавать сложные анимации с Framer Motion или настраивать плавную прокрутку с Lenis, нам необходимо построить прочный фундамент. Эффектное портфолио — это не только красивая картинка, но и производительность, масштабируемость и чистота кода.

В этой статье мы разберем «большую тройку» нашего стека: TypeScript, React и Next.js (с использованием App Router). Мы не будем читать документацию вслух, а сфокусируемся на архитектурных принципах, которые критически важны для современного веб-разработчика.

TypeScript: Ваша страховка от ошибок

Многие новички воспринимают TypeScript как «JavaScript, который заставляет писать больше кода». Однако в контексте профессиональной разработки, TypeScript — это инструмент документации и предотвращения ошибок на этапе написания кода, а не его выполнения.

Статическая типизация против Динамической

JavaScript — язык с динамической типизацией. Это значит, что переменная может хранить число, затем строку, а затем объект. Это удобно для быстрых скриптов, но губительно для крупных приложений.

TypeScript вводит строгую типизацию. Если вы определили, что функция принимает объект с данными о проекте, вы физически не сможете передать туда что-то другое.

Рассмотрим пример компонента карточки проекта для портфолио:

// Определение структуры данных проекта
interface Project {
  id: number;
  title: string;
  description: string;
  tags: string[];
  link?: string; // Опциональное поле
}

// Типизация пропсов компонента
interface ProjectCardProps {
  data: Project;
  isFeatured: boolean;
}

const ProjectCard = ({ data, isFeatured }: ProjectCardProps) => {
  return (
    <div className={isFeatured ? "featured" : "regular"}>
      <h3>{data.title}</h3>
      <p>{data.description}</p>
      {/* TypeScript подскажет, что data.tags — это массив */}
      {data.tags.map(tag => <span key={tag}>{tag}</span>)}
    </div>
  );
};

Интерфейсы и Типы

В нашем курсе мы будем часто использовать ключевое слово interface для описания форм объектов (например, пропсов компонентов) и type для создания союзов (Unions) или примитивов.

TypeScript не запускается в браузере. Он компилируется (транспилируется) в обычный JavaScript. Браузер никогда не узнает о ваших интерфейсах, они нужны только вам и вашему редактору кода.

React: Мышление компонентами

React изменил способ создания интерфейсов, перейдя от манипуляции DOM-деревом к декларативному описанию состояния. Для нашего портфолио важно понимать три кита React: Компоненты, Пропсы и Состояние.

Декларативный подход

Вместо того чтобы говорить браузеру «найди элемент с ID 'button' и добавь ему класс 'active'», мы говорим: «Кнопка должна быть активной, если переменная isActive равна true». React сам решит, как обновить DOM.

Хуки: Управление жизненным циклом

В функциональных компонентах мы используем хуки. Для портфолио нам понадобятся в основном:

  1. useState: Для хранения локального состояния (например, открыто ли модальное окно).
  2. useEffect: Для выполнения побочных эффектов (например, запуск анимации при появлении элемента или подписка на событие скролла).
  3. useRef: Для прямого доступа к DOM-элементу (критично для анимаций с GSAP или Lenis).

Пример использования useRef для анимации:

import { useRef, useEffect } from 'react';

const HeroSection = () => {
  // Создаем ссылку на DOM-элемент.
  // HTMLDivElement указывает TypeScript, какой именно это элемент.
  const titleRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (titleRef.current) {
      console.log("Элемент смонтирован:", titleRef.current);
      // Здесь будет код анимации
    }
  }, []);

  return <h1 ref={titleRef}>Привет, я разработчик</h1>;
};

Архитектура Next.js: App Router

Next.js — это мета-фреймворк поверх React. Если React — это библиотека для создания UI, то Next.js — это полноценный каркас для создания веб-приложений. В версии 13+ был представлен App Router, который мы и будем использовать.

Файловая маршрутизация

В Next.js структура папок определяет URL вашего сайта. Это интуитивно понятно и удобно.

  • app/page.tsx ightarrow ightarrow Главная страница (/)
  • app/about/page.tsx ightarrow ightarrow Страница «Обо мне» (/about)
  • app/projects/[id]/page.tsx ightarrow ightarrow Динамический маршрут (/projects/1, /projects/cool-app)

Server Components (RSC) vs Client Components

Это самая важная концепция в современном Next.js. По умолчанию все компоненты в папке app являются Серверными Компонентами (React Server Components).

Характеристика Server Components (RSC) Client Components
Где рендерятся На сервере (один раз при запросе) На клиенте (в браузере) и сервере (гидратация)
Доступ к БД/Файлам Прямой доступ Нет доступа
Размер JS-бандла Не влияет (код не летит в браузер) Увеличивает размер бандла
Интерактивность Нет (нельзя использовать onClick, useState) Да (полная интерактивность)

Для портфолио это разделение критично. Тяжелые вычисления, получение данных о проектах из CMS или Markdown-файлов мы делаем в серверных компонентах. Анимации, слайдеры и формы обратной связи — в клиентских.

Чтобы превратить компонент в клиентский, нужно добавить директиву 'use client' в первой строке файла.

'use client'; // Магия происходит здесь

import { useState } from 'react';

export default function LikeButton() {
  const [likes, setLikes] = useState(0);
  return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
}

Специальные файлы

В App Router есть зарезервированные имена файлов:

  • layout.tsx: Обертка для страниц. Здесь живут <html>, <body>, навигация и футер. Layout не перерисовывается при переходе между страницами внутри него, что идеально для сохранения состояния анимаций переходов.
  • page.tsx: Уникальный UI для конкретного маршрута.
  • loading.tsx: Показывается, пока грузится содержимое page.tsx.
  • not-found.tsx: Кастомная страница 404.

Tailwind CSS: Утилитарный подход

Вместо написания CSS-файлов и придумывания имен классов (.wrapper-main-content-left), мы используем утилитарные классы прямо в JSX.

Пример:

// Обычный CSS подход
<div className="card">

// Tailwind CSS подход
<div className="bg-white rounded-xl shadow-md p-6 hover:scale-105 transition-transform">

Это ускоряет разработку и гарантирует, что стили не будут конфликтовать. В сочетании с компонентами React, мы получаем изолированные, стилизованные блоки, которые легко переиспользовать.

Подготовка окружения

Для начала работы нам потребуется Node.js. Мы создадим проект следующей командой:

npx create-next-app@latest my-portfolio

При установке выберите следующие опции:

  • TypeScript: Yes
  • ESLint: Yes
  • Tailwind CSS: Yes
  • src/ directory: No (или Yes, по вкусу, но в курсе мы будем работать в корне)
  • App Router: Yes
  • Customize default import alias: No (оставим @/*)

Заключение

Мы разобрали фундамент. TypeScript обеспечит надежность данных, React даст нам компонентную модель, а Next.js App Router возьмет на себя маршрутизацию и оптимизацию производительности. Понимание разницы между Server и Client Components — ключ к созданию быстрого портфолио, которое нравится поисковикам и пользователям.

В следующей статье мы начнем настраивать базовый макет и интегрировать Lenis для создания той самой «дорогой» плавной прокрутки.

Визуальный стиль: Продвинутая верстка с Tailwind CSS и компонентный подход

Визуальный стиль: Продвинутая верстка с Tailwind CSS и компонентный подход

В предыдущем модуле мы заложили прочный фундамент, настроив Next.js, TypeScript и разобравшись с архитектурой приложения. Теперь пришло время вдохнуть жизнь в наш проект. Портфолио встречают по одежке, и именно визуальный стиль определяет первое впечатление работодателя или клиента.

В этой статье мы отойдем от базового использования CSS и погрузимся в профессиональную работу с Tailwind CSS. Мы научимся создавать масштабируемые дизайн-системы, решать проблемы конфликтов стилей и использовать продвинутые техники верстки, которые сделают ваш сайт не только красивым, но и удобным для поддержки.

Философия Utility-First: Почему не БЭМ?

Если вы пришли из мира классического CSS или SCSS, подход Tailwind может показаться хаотичным. Вместо семантических классов вроде .portfolio-card__title, мы пишем .text-xl .font-bold .mb-2. Почему это стало стандартом индустрии?

  1. Отсутствие контекстного переключения. Вам не нужно прыгать между файлом компонента (.tsx) и файлом стилей (.css). Структура и стиль живут в одном месте.
  2. Меньший размер бандла. Tailwind генерирует только те классы, которые вы реально используете. В отличие от традиционного CSS, который только растет с добавлением новых фич, CSS-файл Tailwind стабилизируется.
  3. Системность. Вы не придумываете «волшебные числа» (например, margin: 13px). Вы используете токены из дизайн-системы (m-4 = 1rem = 16px).

Настройка темы: Ваш уникальный бренд

Портфолио должно быть уникальным. Использование стандартных цветов Tailwind сделает ваш сайт похожим на тысячи других. Вся магия кастомизации происходит в файле tailwind.config.ts.

Мы расширим стандартную тему, добавив свои цвета и шрифты, вместо того чтобы перезаписывать их.

import type { Config } from "tailwindcss";

const config: Config = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {
      colors: {
        // Семантические имена лучше, чем описание цвета
        primary: "#0F172A",
        accent: "#38BDF8",
        surface: "#1E293B",
      },
      fontFamily: {
        sans: ["var(--font-inter)"], // Подключение шрифта из Next.js Font
        display: ["var(--font-cal-sans)"],
      },
      backgroundImage: {
        'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
      }
    },
  },
  plugins: [],
};
export default config;

Теперь в коде мы можем использовать bg-primary или text-accent. Если вы захотите провести ребрендинг, достаточно будет поменять hex-коды в одном файле.

Проблема «Длинных строк» и Компонентный подход

Главная претензия к Tailwind — длинные строки классов, загрязняющие JSX. Решение этой проблемы кроется в компонентном подходе React, о котором мы говорили в прошлой статье.

Не копируйте одни и те же классы для каждой кнопки. Создайте компонент UI.

Утилита cn (ClassNames)

В профессиональной разработке на стеке React + Tailwind стандартом де-факто стало использование двух библиотек: clsx (для условного рендеринга классов) и tailwind-merge (для разрешения конфликтов).

Представьте, что у вас есть компонент Button с отступами p-4. Вы хотите использовать его, но в одном месте вам нужен p-2. Если просто склеить строки, в CSS попадут оба класса, и результат будет зависеть от порядка их объявления в таблице стилей, а не от вашего кода.

Создадим утилиту cn в файле lib/utils.ts:

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Теперь мы можем создавать гибкие компоненты:

import { cn } from "@/lib/utils";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "outline";
}

export const Button = ({ className, variant = "primary", ...props }: ButtonProps) => {
  return (
    <button
      className={cn(
        // Базовые стили
        "rounded-lg font-medium transition-all duration-300 active:scale-95",
        "px-6 py-3 flex items-center justify-center",
        // Варианты
        variant === "primary" && "bg-accent text-white hover:bg-accent/90",
        variant === "outline" && "border-2 border-surface text-surface hover:bg-surface/10",
        // Пользовательские классы, которые переопределят базовые при конфликте
        className
      )}
      {...props}
    />
  );
};

Использование:

<Button className="w-full mt-4">Связаться со мной</Button>

Здесь w-full и mt-4 корректно добавятся к кнопке.

Продвинутые селекторы и состояния

Для эффектного портфолио простых ховеров недостаточно. Нам нужны взаимодействия между элементами.

Group и Group-Hover

Частый паттерн: карточка проекта. Мы хотим, чтобы при наведении на всю карточку, увеличивалась картинка и менялся цвет заголовка.

Для этого родительскому элементу даем класс group, а дочерним — group-hover:....

<div className="group relative overflow-hidden rounded-xl bg-surface p-4">
  <div className="overflow-hidden rounded-lg">
    {/* Картинка увеличится при наведении на родительский div */}
    <img
      src="/project.jpg"
      alt="Project"
      className="transition-transform duration-500 group-hover:scale-110"
    />
  </div>

  {/* Текст станет акцентным */}
  <h3 className="mt-4 text-xl font-bold transition-colors group-hover:text-accent">
    Название проекта
  </h3>
</div>

Peer

Модификатор peer позволяет стилизовать элемент в зависимости от состояния его соседа (предыдущего элемента). Это идеально для форм обратной связи.

<form>
  <input
    type="email"
    required
    className="peer border border-gray-300 p-2 invalid:border-red-500"
  />
  {/* Сообщение появится только если инпут невалиден */}
  <span className="hidden text-red-500 peer-invalid:block">
    Пожалуйста, введите корректный email
  </span>
</form>

Адаптивная верстка: Mobile First

Tailwind использует подход Mobile First. Это значит, что классы без префиксов работают на всех экранах (начиная с мобильных), а префиксы (md:, lg:) переопределяют их для больших экранов.

Ошибка новичка: писать sm:w-full md:w-1/2. Правильно: w-full md:w-1/2. w-full уже работает для sm и ниже по умолчанию.

Сетка проектов (Grid)

Для галереи работ лучше всего использовать CSS Grid. Tailwind позволяет делать сложные сетки одной строкой.

<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
  {/* Карточки проектов */}
  <ProjectCard />
  <ProjectCard />
  <ProjectCard />
</div>

Если вы хотите, чтобы сетка заполнялась автоматически в зависимости от ширины карточки (без медиа-запросов), можно использовать произвольные значения (Arbitrary Values):

<div className="grid grid-cols-[repeat(auto-fit,minmax(300px,1fr))] gap-6">

Это создаст адаптивную сетку, где каждая колонка имеет минимум 300px ширины.

Произвольные значения (Arbitrary Values)

Иногда дизайн требует отступов или размеров, которых нет в стандартной шкале. Tailwind позволяет использовать квадратные скобки для "одноразовых" значений.

  • h-[500px] — фиксированная высота.
  • bg-[#123456] — специфичный цвет.
  • z-[100] — специфичный z-index.

Однако, не злоупотребляйте этим. Если вы видите, что постоянно пишете text-[#38BDF8], лучше вынесите этот цвет в tailwind.config.ts.

Темная тема (Dark Mode)

Для разработчика наличие темной темы в портфолио — признак хорошего тона. В Tailwind это реализуется через класс dark.

В tailwind.config.ts добавьте:

darkMode: 'class',

Теперь вы можете использовать модификатор dark::

<div className="bg-white text-black dark:bg-slate-900 dark:text-white">
  Контент
</div>

Для переключения темы мы будем использовать библиотеку next-themes, которая сохраняет выбор пользователя в LocalStorage и предотвращает "мигание" темы при загрузке.

Заключение

Мы разобрали, как превратить хаос CSS-классов в стройную систему. Использование утилиты cn, правильная настройка конфига и понимание модификаторов group и peer позволят вам верстать сложные интерфейсы с невероятной скоростью.

Ваше домашнее задание — создать компонент Card для будущего портфолио, используя cn для слияния классов и реализовать hover-эффект через group.

В следующей статье мы заставим эти статичные блоки двигаться. Мы подключим Framer Motion и создадим анимации появления, которые вызовут "Wow-эффект" у посетителей.

Искусство анимации: От основ до сложных техник работы с Framer Motion

Искусство анимации: От основ до сложных техник работы с Framer Motion

Добро пожаловать в третий модуль нашего курса «Создание эффектного портфолио». В предыдущих статьях мы построили надежный архитектурный фундамент на Next.js и TypeScript, а также создали стильную дизайн-систему с помощью Tailwind CSS. Но давайте будем честны: в 2024 году просто красивой статичной картинки недостаточно. Чтобы портфолио действительно «цепляло» и выглядело дорого, оно должно быть живым.

Сегодня мы погрузимся в мир Framer Motion — самой мощной библиотеки анимаций для React. Мы пройдем путь от простого появления элементов до сложных скролл-эффектов и параллакса, которые заставят рекрутеров задержаться на вашем сайте.

Почему Framer Motion?

Вы можете спросить: «Зачем мне библиотека, если есть CSS Transitions?».

CSS отлично подходит для простых состояний (например, изменение цвета кнопки при наведении). Но когда речь заходит о последовательной анимации списка проектов, переходов между страницами или реакции на скролл, CSS становится громоздким и трудно поддерживаемым.

Framer Motion предлагает декларативный подход. Вместо того чтобы описывать как менять пиксели, вы описываете состояния, в которых должен находиться элемент. Библиотека сама рассчитывает физику движения.

Фундамент: Компонент motion

Сердце библиотеки — это компонент motion. Это обертка над стандартными HTML-элементами. Если вы хотите анимировать div, вы используете motion.div. Если h1 — motion.h1.

У этого компонента есть три главных пропса:

  1. initial: Состояние элемента до начала анимации (например, прозрачный и смещенный вниз).
  2. animate: Конечное состояние, к которому элемент должен прийти.
  3. transition: Настройки физики (длительность, задержка, тип анимации).

Рассмотрим пример появления заголовка:

'use client'; // Framer Motion работает только в клиентских компонентах

import { motion } from 'framer-motion';

export const HeroTitle = () => {
  return (
    <motion.h1
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      className="text-4xl font-bold"
    >
      Привет, я разработчик
    </motion.h1>
  );
};

В этом примере заголовок появится из прозрачности и плавно поднимется на 20 пикселей вверх.

Оркестрация с помощью Variants

Когда у вас один элемент, пропсы initial и animate работают отлично. Но представьте, что у вас есть список из 10 карточек проектов, и вы хотите, чтобы они появлялись по очереди (каскадом). Копировать код в каждую карточку — плохая практика.

Здесь на сцену выходят Variants (Варианты). Это способ вынести настройки анимации в отдельный объект и управлять ими через имена состояний.

Главная магия вариантов — пробрасывание анимации. Если вы зададите родительскому элементу состояние visible, все дочерние элементы тоже попытаются перейти в состояние visible, если у них есть такой вариант.

const containerVariants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: {
      // staggerChildren делает задержку между анимациями детей
      staggerChildren: 0.2
    }
  }
};

const itemVariants = {
  hidden: { opacity: 0, x: -50 },
  visible: { opacity: 1, x: 0 }
};

export const ProjectList = () => {
  return (
    <motion.div
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="grid grid-cols-2 gap-4"
    >
      {[1, 2, 3, 4].map((id) => (
        // Нам не нужно указывать initial/animate здесь,
        // они наследуются от родителя по именам ключей
        <motion.div
          key={id}
          variants={itemVariants}
          className="bg-gray-100 p-4 rounded-xl"
        >
          Проект {id}
        </motion.div>
      ))}
    </motion.div>
  );
};

Использование staggerChildren создает тот самый профессиональный эффект «лесенки», который так любят дизайнеры.

Анимация при скролле: whileInView

В портфолио важно не загружать пользователя анимациями сразу. Контент должен оживать по мере того, как пользователь скроллит страницу. Раньше для этого использовали Intersection Observer, но в Framer Motion это делается одним пропсом: whileInView.

<motion.div
  initial={{ opacity: 0, scale: 0.8 }}
  whileInView={{ opacity: 1, scale: 1 }}
  viewport={{ once: true, amount: 0.5 }}
>
  Я появлюсь, когда меня будет видно наполовину
</motion.div>

Настройка viewport={{ once: true }} гарантирует, что анимация проиграется только один раз. Без этого флага элемент будет исчезать и появляться каждый раз, когда вы прокручиваете его туда-сюда, что часто раздражает пользователей.

Магия Parallax и useTransform

Параллакс — это эффект, при котором фоновые элементы движутся медленнее (или в другом направлении), чем передний план. Это создает ощущение глубины.

Для создания таких эффектов мы используем хуки useScroll и useTransform. Это требует понимания простой математической концепции: линейной интерполяции.

Когда мы скроллим страницу, значение скролла меняется, например, от 0 до 1000 пикселей. Нам нужно преобразовать («спроецировать») это значение в другое, например, в угол поворота элемента.

Формула линейной интерполяции выглядит так:

y=y1+(x−x1)(y2−y1)x2−x1y = y_1 + \frac{(x - x_1)(y_2 - y_1)}{x_2 - x_1}

Где:

  • yy — искомое значение (например, угол поворота).
  • xx — текущее входное значение (позиция скролла).
  • x1,x2x_1, x_2 — диапазон входных значений (начало и конец скролла).
  • y1,y2y_1, y_2 — диапазон выходных значений (начальный и конечный угол).

К счастью, Framer Motion делает эти вычисления за нас.

import { useScroll, useTransform, motion } from 'framer-motion';
import { useRef } from 'react';

export const ParallaxSection = () => {
  const ref = useRef(null);

  // Отслеживаем прогресс скролла конкретного элемента
  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ["start end", "end start"]
  });

  // Преобразуем прогресс от 0 до 1 в смещение по Y от -100px до 100px
  const y = useTransform(scrollYProgress, [0, 1], [-100, 100]);

  return (
    <div ref={ref} className="h-screen flex items-center justify-center overflow-hidden">
      <motion.div style={{ y }} className="absolute z-0">
        <img src="/background-shape.png" alt="bg" />
      </motion.div>
      <div className="z-10 bg-white p-10 rounded shadow">
        Контент, который движется с нормальной скоростью
      </div>
    </div>
  );
};

В этом примере фоновая картинка будет двигаться против направления скролла, создавая эффект глубины.

Layout Animations: Магия перестроения

Одна из самых сложных задач в веб-разработке — анимировать изменение структуры DOM. Например, когда вы фильтруете список проектов, и одни карточки исчезают, а другие плавно занимают их места.

В Framer Motion для этого достаточно добавить проп layout.

<motion.div layout className="grid grid-cols-3 gap-4">
  {filteredProjects.map(project => (
    <motion.div
      layout
      key={project.id}
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      exit={{ opacity: 0 }}
    >
      {project.title}
    </motion.div>
  ))}
</motion.div>

Когда массив filteredProjects изменится, Framer Motion автоматически вычислит новую позицию для каждого элемента и плавно переместит его туда. Без пропа layout элементы бы просто мгновенно перепрыгнули.

Shared Layout Animations

Вы наверняка видели эффект в App Store, когда при клике на маленькую карточку она разворачивается на весь экран. Это называется Shared Element Transition.

Чтобы связать два разных компонента (маленькую карточку в списке и большую открытую карточку), нужно дать им одинаковый layoutId.

// В списке
<motion.div layoutId={`project-${id}`} onClick={openProject}>
  <img src={image} />
</motion.div>

// В модальном окне
{isOpen && (
  <motion.div layoutId={`project-${id}`} className="fullscreen-modal">
    <img src={image} />
  </motion.div>
)}

Framer Motion поймет, что это «один и тот же» элемент, и плавно трансформирует маленький div в большой, искажая размеры и позицию.

Оптимизация и Accessibility

Анимации — это прекрасно, но они могут нагружать процессор и вызывать головокружение у некоторых пользователей.

LazyMotion

Чтобы уменьшить размер JS-бандла, используйте LazyMotion. Это позволяет загружать код анимаций только тогда, когда они действительно нужны.

Reduced Motion

Всегда уважайте настройки пользователя. Если человек отключил анимации в системе (режим «Reduced Motion»), ваше портфолио не должно мелькать.

import { useReducedMotion } from "framer-motion";

const shouldReduceMotion = useReducedMotion();

const variants = {
  visible: { opacity: 1, x: 0 },
  hidden: {
    opacity: 0,
    x: shouldReduceMotion ? 0 : -100 // Отключаем движение, оставляем только прозрачность
  }
};

Заключение

Мы разобрали основные инструменты Framer Motion, которые понадобятся вам для создания портфолио уровня Awwwards. Мы научились создавать каскадные появления с помощью variants, оживлять скролл через whileInView и useTransform, а также делать сложные перестроения макета одной строчкой кода layout.

Ваше домашнее задание: возьмите сетку проектов из предыдущего урока и добавьте к ней анимацию появления staggerChildren, а также эффект параллакса для заголовка главной страницы.

В следующей, завершающей статье технического блока, мы разберем Lenis — библиотеку для плавной инерционной прокрутки, которая свяжет все наши анимации воедино и придаст сайту то самое «премиальное» ощущение.

Премиальный UX: Интеграция плавного скролла Lenis и параллакс-эффекты

Премиальный UX: Интеграция плавного скролла Lenis и параллакс-эффекты

Добро пожаловать в четвертый, завершающий технический модуль нашего курса «Создание эффектного портфолио». Мы уже проделали огромный путь: построили архитектуру на Next.js и TypeScript, создали дизайн-систему с Tailwind CSS и научились анимировать элементы с Framer Motion.

Однако, если вы откроете наше портфолио сейчас и начнете прокручивать страницу, вы можете заметить, что чего-то не хватает. Скролл кажется «дерганым», резким, сугубо механическим. В мире премиального веб-дизайна (сайты Awwwards, портфолио топ-студий) прокрутка ощущается иначе — она имеет вес, инерцию и плавность.

Сегодня мы добавим этот финальный штрих с помощью библиотеки Lenis, разберем математику плавности и создадим сложные параллакс-эффекты, которые работают в симбиозе с нативной прокруткой.

Проблема стандартного скролла

Браузерный скролл по умолчанию линеен. Когда вы крутите колесико мыши, браузер смещает страницу на фиксированное количество пикселей. Это эффективно, но скучно. Это не передает ощущение «физики» взаимодействия.

Раньше разработчики использовали библиотеки вроде Locomotive Scroll, которые занимались так называемым Scroll Hijacking (перехватом скролла). Они полностью отключали нативный скролл браузера и эмулировали его с помощью transform: translateY(). Это создавало массу проблем:

  1. Доступность (Accessibility): Ломалась навигация с клавиатуры и скринридеры.
  2. Производительность: Тяжелые вычисления в основном потоке.
  3. Нативные фичи: Переставали работать position: sticky и position: fixed.

Lenis изменил правила игры. Это библиотека нового поколения, которая не заменяет нативный скролл, а интерполирует его значения. Это значит, что все нативные функции браузера (поиск по странице, sticky-элементы, навигация) продолжают работать идеально.

Математика плавности: Линейная интерполяция (Lerp)

Прежде чем писать код, важно понять, как работает плавность. В основе лежит концепция Линейной интерполяции (Linear Interpolation, или Lerp).

Представьте, что вы хотите переместить объект из точки А в точку Б. Вместо мгновенного перемещения, мы каждый кадр сдвигаем объект на определенный процент от оставшегося расстояния.

Формула линейной интерполяции выглядит так:

Vnew=Vcurrent+(Vtarget−Vcurrent)⋅tV_{new} = V_{current} + (V_{target} - V_{current}) \cdot t

Где:

  • VnewV_{new} — новое значение позиции (куда мы сдвинемся в этом кадре).
  • VcurrentV_{current} — текущая позиция скролла.
  • VtargetV_{target} — целевая позиция (куда пользователь крутанул колесико).
  • tt — коэффициент сглаживания (обычно от 0.01 до 0.1). Чем меньше число, тем плавнее (и медленнее) движение.

Lenis выполняет эту формулу внутри цикла анимации браузера (requestAnimationFrame), обновляя позицию скролла 60-120 раз в секунду, создавая иллюзию инерции.

Интеграция Lenis в Next.js App Router

Поскольку Lenis работает с окном браузера (window), нам нужно инициализировать его в клиентском компоненте. В архитектуре Next.js 13+ мы не можем просто добавить код в layout.tsx, так как это серверный компонент.

Создадим специальный компонент-обертку.

Файл: src/components/providers/SmoothScroll.tsx

'use client';

import { ReactNode, useEffect } from 'react';
import Lenis from 'lenis';

export default function SmoothScroll({ children }: { children: ReactNode }) {
  useEffect(() => {
    // Инициализация Lenis
    const lenis = new Lenis({
      duration: 1.2, // Длительность инерции (секунды)
      easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // Функция плавности
      orientation: 'vertical',
      gestureOrientation: 'vertical',
      smoothWheel: true,
    });

    // Цикл анимации (Game Loop для скролла)
    function raf(time: number) {
      lenis.raf(time);
      requestAnimationFrame(raf);
    }

    requestAnimationFrame(raf);

    // Очистка при размонтировании
    return () => {
      lenis.destroy();
    };
  }, []);

  return <>{children}</>;
}

Теперь обернем наше приложение в этот провайдер в корневом макете.

Файл: src/app/layout.tsx

import SmoothScroll from '@/components/providers/SmoothScroll';

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ru">
      <body>
        <SmoothScroll>
          {children}
        </SmoothScroll>
      </body>
    </html>
  );
}

Поздравляю! Теперь все ваше портфолио имеет приятную, «дорогую» инерцию при прокрутке.

Симбиоз Lenis и Framer Motion

В предыдущей статье мы использовали хук useScroll из Framer Motion. Хорошая новость: Lenis и Framer Motion отлично дружат, так как Lenis обновляет нативный скролл, который слушает Framer.

Однако, для сложных эффектов иногда требуется синхронизация. Давайте создадим продвинутый компонент ParallaxImage, который использует плавность Lenis для создания эффекта глубины.

Создание компонента ParallaxImage

Идея: контейнер картинки имеет фиксированную высоту, а сама картинка внутри него выше контейнера и движется с другой скоростью при скролле.

'use client';

import { useRef } from 'react';
import { motion, useScroll, useTransform } from 'framer-motion';

interface ParallaxImageProps {
  src: string;
  alt: string;
  className?: string;
}

export const ParallaxImage = ({ src, alt, className }: ParallaxImageProps) => {
  const containerRef = useRef<HTMLDivElement>(null);

  // Отслеживаем прогресс скролла относительно контейнера
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["start end", "end start"] // Начинаем, когда верх контейнера входит в низ экрана
  });

  // Трансформируем 0 -> 1 (прогресс) в пиксели смещения
  // Картинка сдвинется на 20% своей высоты
  const y = useTransform(scrollYProgress, [0, 1], ["-20%", "20%"]);

  return (
    <div
      ref={containerRef}
      className={`relative overflow-hidden ${className}`} // overflow-hidden обрезает лишнее
    >
      <motion.div style={{ y }} className="h-[140%] w-full relative -top-[20%]">
        <img
          src={src}
          alt={alt}
          className="w-full h-full object-cover"
        />
      </motion.div>
    </div>
  );
};

В этом примере мы используем h-[140%] (высота 140% от родителя) и сдвигаем картинку внутри маски. Благодаря Lenis, обновление значений scrollYProgress будет происходить плавно, без рывков.

Эффект Sticky Scroll (Липкий скролл)

Один из самых популярных паттернов в портфолио 2024 года — секции, которые «наслаиваются» друг на друга. Благодаря тому, что Lenis поддерживает нативный CSS, нам не нужен JavaScript для базовой логики.

Достаточно использовать position: sticky.

export const StickySection = () => {
  return (
    <section className="relative">
      {/* Карточка 1 */}
      <div className="sticky top-0 h-screen bg-blue-500 flex items-center justify-center text-white">
        <h2>Проект 1</h2>
      </div>

      {/* Карточка 2 перекроет первую */}
      <div className="sticky top-0 h-screen bg-green-500 flex items-center justify-center text-white">
        <h2>Проект 2</h2>
      </div>

      {/* Карточка 3 перекроет вторую */}
      <div className="sticky top-0 h-screen bg-purple-500 flex items-center justify-center text-white">
        <h2>Проект 3</h2>
      </div>
    </section>
  );
};

Lenis сделает момент «прилипания» карточек мягким, убирая микро-дрожание, которое иногда возникает в стандартном браузере.

Оптимизация и подводные камни

Интеграция плавного скролла требует ответственности. Вот несколько правил, которые спасут UX вашего портфолио:

1. Не блокируйте скролл во время загрузки

Некоторые разработчики скрывают overflow у body до полной загрузки сайта. Не делайте этого. Пользователь должен иметь контроль над интерфейсом сразу.

2. Уважайте настройки движения

Некоторых пользователей укачивает от параллакса и плавного скролла. Мы должны проверять медиа-запрос prefers-reduced-motion.

В нашем компоненте SmoothScroll:

// Внутри useEffect
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

if (prefersReducedMotion) {
  // Не инициализируем Lenis или ставим duration: 0
  return;
}

3. Изоляция горизонтального скролла

Если у вас есть горизонтальный слайдер внутри вертикальной страницы, Lenis может конфликтовать с ним. Чтобы этого избежать, добавьте атрибут data-lenis-prevent к контейнеру, который должен скроллиться нативно.

<div className="overflow-x-auto flex gap-4" data-lenis-prevent>
  {/* Слайды */}
</div>

Заключение технического блока

Мы завершили техническую настройку нашего портфолио. Теперь у вас есть:

  1. Next.js App Router для быстрой навигации и SEO.
  2. TypeScript для надежности кода.
  3. Tailwind CSS для стильной и быстрой верстки.
  4. Framer Motion для анимации появлений и переходов.
  5. Lenis для премиального ощущения от взаимодействия.

Этот стек — «золотой стандарт» для frontend-разработчика, желающего продемонстрировать высокий уровень навыков. Вы не просто сверстали макет, вы создали опыт (Experience).

В следующих модулях мы отойдем от кода и поговорим о наполнении: как писать кейсы, какие проекты выбирать и как «продать» себя работодателю через контент. Но ваш технический фундамент теперь безупречен.

Продакшн: Оптимизация производительности, SEO и деплой готового портфолио

Продакшн: Оптимизация производительности, SEO и деплой готового портфолио

Поздравляю! Вы прошли долгий путь. Мы начали с настройки TypeScript и Next.js, создали уникальную дизайн-систему на Tailwind CSS, оживили интерфейс с помощью Framer Motion и добавили премиальную плавность с Lenis. Ваше портфолио выглядит потрясающе на локальном сервере localhost:3000.

Но «работает у меня на машине» — это не тот результат, который мы ищем. Профессиональное портфолио отличается не только визуальным стилем, но и тем, как быстро оно загружается, насколько хорошо индексируется поисковиками и как стабильно работает в реальном мире.

В этой финальной статье мы превратим ваш проект в полноценный продукт. Мы займемся оптимизацией Core Web Vitals, настроим SEO-метаданные для социальных сетей и задеплоим сайт в глобальную сеть с помощью Vercel.

Оптимизация производительности: Борьба за миллисекунды

Красивые анимации не имеют значения, если сайт грузится 5 секунд. Пользователи (и рекрутеры) нетерпеливы. Next.js предоставляет мощные инструменты для оптимизации «из коробки», но их нужно уметь использовать.

1. Оптимизация изображений с next/image

Самая частая ошибка новичков — использование стандартного тега <img>. В современном вебе изображения должны быть адаптивными, сжатыми и в современных форматах (AVIF, WebP).

Компонент Image из Next.js делает это автоматически:

  1. Конвертация форматов: Автоматически отдает браузеру WebP или AVIF вместо тяжелых JPEG/PNG.
  2. Предотвращение сдвига макета (CLS): Резервирует место под картинку до её загрузки.
  3. Lazy Loading: Загружает изображения только тогда, когда они попадают в область видимости.

Пример использования:

import Image from 'next/image';
import heroPic from '@/assets/hero.jpg';

export default function Hero() {
  return (
    <div className="relative h-96 w-full">
      <Image
        src={heroPic}
        alt="Портрет разработчика"
        fill // Заполняет родительский контейнер
        priority // Загружать сразу (для первого экрана)
        placeholder="blur" // Показывает размытую версию при загрузке
        className="object-cover"
      />
    </div>
  );
}

Важно: Всегда добавляйте проп priority для изображений, которые видны на первом экране (LCP — Largest Contentful Paint). Это критически влияет на оценку Google Lighthouse.

2. Шрифты и next/font

Подключение шрифтов через Google Fonts в CSS часто приводит к проблеме FOIT (Flash of Invisible Text) или FOUT (Flash of Unstyled Text). Текст сначала не виден или отображается стандартным шрифтом, а потом «скачет», когда загружается нужный файл.

Next.js решает это через модуль next/font. Он скачивает файлы шрифтов во время сборки (build time) и хостит их вместе с вашим сайтом. Никаких лишних запросов к Google.

// src/app/layout.tsx
import { Inter, Calistoga } from 'next/font/google';

const inter = Inter({
  subsets: ['latin', 'cyrillic'],
  variable: '--font-inter' // CSS-переменная для Tailwind
});

const calistoga = Calistoga({
  weight: '400',
  subsets: ['latin'],
  variable: '--font-calistoga'
});

export default function RootLayout({ children }) {
  return (
    <html lang="ru" className={`${inter.variable} ${calistoga.variable}`}>
      <body>{children}</body>
    </html>
  );
}

3. Разделение кода (Code Splitting) и next/dynamic

Если в вашем портфолио есть тяжелые компоненты (например, 3D-сцена на Three.js или сложный график), не стоит загружать их в основной бандл. Используйте ленивую загрузку.

import dynamic from 'next/dynamic';

// Этот компонент загрузится только тогда, когда понадобится
const Heavy3DModel = dynamic(() => import('@/components/3d/Model'), {
  loading: () => <p>Загрузка 3D...</p>,
  ssr: false // Отключаем серверный рендеринг для 3D-библиотек
});

SEO: Быть найденным

Next.js App Router предоставляет элегантный API для управления метаданными. Вам больше не нужно вручную писать теги <meta> в head.

Metadata API

В любом page.tsx или layout.tsx вы можете экспортировать объект metadata.

import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'Иван Иванов | Frontend Developer',
    template: '%s | Иван Иванов' // Шаблон для вложенных страниц
  },
  description: 'Портфолио Frontend разработчика. React, Next.js, TypeScript.',
  keywords: ['Frontend', 'React', 'Next.js', 'Portfolio'],
  authors: [{ name: 'Ivan Ivanov', url: 'https://ivan.com' }],
  creator: 'Ivan Ivanov',
};

Open Graph: Красивые ссылки в соцсетях

Когда вы скидываете ссылку на портфолио в Telegram или LinkedIn, должна подтягиваться красивая картинка-превью. Это настраивается через поле openGraph.

export const metadata: Metadata = {
  // ...остальные поля
  openGraph: {
    title: 'Иван Иванов | Frontend Developer',
    description: 'Создаю быстрые и красивые веб-интерфейсы',
    url: 'https://ivan.com',
    siteName: 'Portfolio',
    images: [
      {
        url: 'https://ivan.com/og-image.png',
        width: 1200,
        height: 630,
      },
    ],
    locale: 'ru_RU',
    type: 'website',
  },
};

Для динамической генерации картинок (например, для страницы конкретного проекта) можно использовать файл opengraph-image.tsx, который позволяет рисовать картинки с помощью CSS прямо на сервере, но для начала достаточно статической картинки в папке public.

Деплой: Vercel

Next.js создан компанией Vercel, поэтому их платформа — лучшее место для хостинга вашего портфолио. Это бесплатно для личных проектов, невероятно быстро и не требует сложной настройки серверов (DevOps).

Процесс деплоя

  1. GitHub: Убедитесь, что ваш код находится в репозитории на GitHub.
  2. Регистрация: Зайдите на vercel.com и войдите через GitHub.
  3. Импорт: Нажмите «Add New Project» и выберите репозиторий с портфолио.
  4. Настройка:
    • Framework Preset: Next.js (определится автоматически).
    • Environment Variables: Если вы использовали API ключи (например, для отправки формы через EmailJS), добавьте их здесь.
  5. Deploy: Нажмите кнопку «Deploy».

Через минуту ваше портфолио будет доступно по адресу project-name.vercel.app.

CI/CD Pipeline

Самое приятное в Vercel — это автоматизация. Теперь, каждый раз, когда вы делаете git push в ветку main, Vercel автоматически:

  1. Увидит изменения.
  2. Запустит сборку (next build).
  3. Если сборка прошла успешно, обновит сайт.
  4. Если есть ошибки (например, TypeScript ругается на типы), деплой отменится, и вы получите уведомление.

Это и есть профессиональный процесс CI/CD (Continuous Integration / Continuous Delivery).

Аналитика и Speed Insights

После деплоя подключите вкладку Analytics и Speed Insights в панели Vercel. Это даст вам реальные данные о том, как пользователи взаимодействуют с сайтом:

  • Web Vitals: Реальная скорость загрузки у пользователей.
  • Visitors: Откуда приходят люди и какие проекты смотрят чаще всего.

Заключение курса

Мы прошли огромный путь. Вы создали не просто сайт-визитку, а сложный инженерный продукт.

  • TypeScript защищает вас от ошибок.
  • Tailwind CSS позволяет быстро менять дизайн.
  • Framer Motion и Lenis создают «вау-эффект».
  • Next.js обеспечивает мгновенную загрузку и SEO.

Теперь ваша задача — наполнить это портфолио реальными кейсами. Помните: код — это важно, но контент — это король. Описывайте свои проекты так же тщательно, как вы писали код для этого портфолио.

Удачи в поиске работы мечты!