Практика: соберите лендинг на HTML и CSS
Сквозной практический урок: мы соберём с нуля рабочий лендинг условного продукта — «Бота для брони столиков в кафе». Каждый раздел — один шаг: от пустого файла до адаптивной страницы, готовой к публикации. Код постепенно растёт, и после каждого крупного этапа вы можете запустить его прямо здесь, в песочнице.
1. Постановка задачи и структура страницы
Лендинг — это одностраничный сайт, который продаёт одно конкретное действие: оставить заявку, купить, подписаться. У нашего продукта — бота для брони столиков в кафе — цель простая: посетитель должен оставить заявку на подключение. Весь лендинг подводит к этому действию.
Блоки лендинга
- Шапка — логотип и меню, всегда доступное при скролле;
- Герой — первый экран: суть продукта и главная кнопка;
- Преимущества — 3–4 карточки «почему мы»;
- Тарифы — цены и выбор плана;
- Отзывы — социальное доказательство;
- Форма — та самая заявка, ради которой всё собрано;
- Подвал — копирайт, меню, навигация.
<!-- скелет лендинга: семь контейнеров в <body> -->
<header class="site-header"></header>
<section class="hero"></section>
<section class="features"></section>
<section class="pricing"></section>
<section class="reviews"></section>
<section class="order"></section>
<footer class="site-footer"></footer>Пайплайн
- Разметка — сначала весь HTML, ни строчки стилей: так видна структура без картинки;
- Стили — переменные, сброс, затем секции сверху вниз;
- Адаптив — media queries под планшет и телефон;
- Проверка — доступность, скорость, валидность.
index.html: отдельный style.css понадобится, когда лендинг станет большим. Сейчас важнее меньше переключаться между файлами.2. Скелет и палитра
Любой дизайн управляется палитрой. Правило 60-30-10: 60 % — нейтральный фон и текст, 30 % — подложки секций, 10 % — редкий акцент. Всю палитру выносим в CSS-переменные: поменять фирменный цвет — одна строчка.
/* палитра 60-30-10: 60% нейтрали, 30% подложки, 10% акцент */
:root {
--bg: #ffffff; /* 60% - фоны и текст */
--text: #1f2937;
--text-muted: #6b7280;
--surface: #f6f7f9; /* 30% - секции-подложки */
--border: #e5e7eb;
--accent: #2563eb; /* 10% - редкий акцент */
--accent-dark: #1d4ed8;
--accent-soft: #e8f0fe;
}
/* базовый сброс: убираем отступы, фиксируем логику размеров */
* { margin: 0; padding: 0; box-sizing: border-box; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
/* один контейнер на весь лендинг */
.container {
max-width: 1080px;
margin: 0 auto;
padding: 0 20px;
}Без сброса браузер добавит свои отступы спискам и заголовкам, и «проценты» палитры поплывут. box-sizing: border-box делает ширину предсказуемой: паддинг больше не раздувает блок за пределы.
3. Шапка и логотип
Шапка липкая: при скролле она уходит, но всегда остаётся на экране. Раскладываем её флексом — логотип слева, меню справа. Активная ссылка получает класс .active.
.site-header {
position: sticky; /* «прилипаем» к верху окна */
top: 0;
z-index: 100;
background: var(--bg);
border-bottom: 1px solid var(--border);
}
.site-header .container {
display: flex; /* логотип и меню в один ряд */
align-items: center;
justify-content: space-between;
height: 64px;
}
.logo { font-weight: 700; font-size: 1.15rem; color: var(--accent); }
.nav a { margin-left: 24px; color: var(--text-muted); font-weight: 500; }
.nav a:hover { color: var(--text); }
.nav a.active { color: var(--accent); } /* текущий пункт меню */<header class="site-header">
<div class="container">
<a class="logo" href="#">🍽 Столик</a>
<nav class="nav">
<a href="#features">Преимущества</a>
<a href="#pricing">Тарифы</a>
<a href="#order">Подключить</a>
</nav>
</div>
</header>.active — не про зависшее состояние, а про ориентир: любой пункт меню сейчас ведёт на якорь своей секции. Когда у лендинга появится несколько страниц, тот же класс будет отмечать текущую страницу.#features, #pricing. Браузер плавно… точнее, сразу — но в разделе 11 мы добавим scroll-behavior.4. Герой-секция
Герой — это «первый экран». За 5 секунд он должен ответить на три вопроса: что это, для кого, что делать. Поэтому здесь только заголовок, подзаголовок и пара кнопок. Центрируем строки, а фон заливаем градиентом.
.hero {
padding: 96px 0;
text-align: center; /* центрируем строки */
background: linear-gradient(135deg,
var(--accent) 0%, #0f3fa8 100%);
color: #fff;
}
.hero h1 { font-size: 2.6rem; max-width: 720px; margin: 0 auto 16px; }
.hero p { font-size: 1.15rem; max-width: 560px; margin: 0 auto 28px; opacity: .92; }
/* пара CTA: основная кнопка + «призрак» */
.btn { display: inline-block; padding: 12px 24px; border: 0; border-radius: 10px;
font-weight: 600; text-decoration: none; cursor: pointer; }
.btn-primary { background: #fff; color: var(--accent-dark); }
.btn-ghost { background: transparent; border: 2px solid rgba(255, 255, 255, .7); color: #fff; }<section class="hero">
<div class="container">
<h1>Бронируйте столики без телефонных звонков</h1>
<p>Гость открывает чат-бота, выбирает зал и время —<br>
столик закреплён за 10 секунд.</p>
<a class="btn btn-primary" href="#order">Подключить за 15 минут</a>
<a class="btn btn-ghost" href="#pricing">Смотреть тарифы</a>
</div>
</section>5. Преимущества на flex
Секция преимуществ — сетка карточек: иконка, заголовок, короткий текст. Флекс с flex-wrap и базовым размером flex: 1 1 240px сам решает, сколько карточек поместится в строку. На большом экране — четыре в ряд, на узком — перенос.
.features { padding: 72px 0; background: var(--surface); }
.features-grid {
display: flex;
flex-wrap: wrap; /* карточки переносятся на новую строку */
gap: 24px;
}
.feature-card {
flex: 1 1 240px; /* базовый размер, дальше - растём и переносимся */
padding: 24px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 14px;
}
.feature-card:hover { transform: translateY(-4px); box-shadow: 0 8px 24px rgba(31, 41, 55, .08); }
.feature-icon { font-size: 2rem; } /* эмодзи вместо иконки-файла */
.feature-card h3 { margin: 12px 0 8px; }
.feature-card p { color: var(--text-muted); font-size: .95rem; }<section class="features" id="features">
<div class="container">
<h2>Что умеет бот</h2>
<div class="features-grid">
<article class="feature-card">
<span class="feature-icon">⚡</span>
<h3>Бронь за 10 секунд</h3>
<p>Гость выбирает зал и время — столик сразу закреплён.</p>
</article>
<article class="feature-card">
<span class="feature-icon">📅</span>
<h3>Синхронизация с залом</h3>
<p>Расписание столиков всегда актуально, без двойных броней.</p>
</article>
<article class="feature-card">
<span class="feature-icon">🔔</span>
<h3>Напоминания гостям</h3>
<p>Бот напомнит о брони за час — «неявок» станет меньше.</p>
</article>
<article class="feature-card">
<span class="feature-icon">📊</span>
<h3>Отчёты об ужинах</h3>
<p>Сколько столиков занято вечером — одним движением.</p>
</article>
</div>
</div>
</section>6. Тарифы на grid
Тарифы — это уже «каркас»: название, цена, список, кнопка. Здесь grid со схемой auto-fit, minmax(260px, 1fr) даёт настоящий адаптив без единого media query: колонки добавляются или убираются сами. Список растягиваем флексом, чтобы кнопки выровнялись по низу.
.pricing { padding: 72px 0; }
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
}
.plan {
display: flex;
flex-direction: column; /* цена, список, кнопка - в колонку */
padding: 28px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 14px;
}
.plan.featured { border-color: var(--accent); box-shadow: 0 8px 24px rgba(31, 41, 55, .08); }
.plan .price { margin: 8px 0 16px; font-size: 2rem; font-weight: 700; }
.plan ul { margin-bottom: 24px; flex: 1; } /* растягивает список и прижимает кнопку вниз */
.plan li { padding: 6px 0; color: var(--text-muted); }
.plan li::before { content: "✓ "; color: var(--accent); font-weight: 700; }
.plan .btn { width: 100%; text-align: center; }<section class="pricing" id="pricing">
<div class="container">
<h2>Тарифы</h2>
<div class="pricing-grid">
<div class="plan">
<h3>Кафе</h3>
<p class="price">1 490 ₽/мес</p>
<ul>
<li>До 50 броней в день</li>
<li>Один зал</li>
<li>Уведомления в Telegram</li>
</ul>
<a class="btn btn-primary" href="#order">Выбрать план</a>
</div>
<div class="plan featured">
<h3>Ресторан</h3>
<p class="price">2 990 ₽/мес</p>
<ul>
<li>Безлимит броней</li>
<li>3 зала и расписание смен</li>
<li>СМС-напоминания гостям</li>
<li>Отчёты о загрузке</li>
</ul>
<a class="btn btn-primary" href="#order">Выбрать план</a>
</div>
<div class="plan">
<h3>Сеть</h3>
<p class="price">5 990 ₽/мес</p>
<ul>
<li>Всё из тарифа «Ресторан»</li>
<li>До 10 заведений</li>
<li>Общая аналитика</li>
</ul>
<a class="btn btn-primary" href="#order">Выбрать план</a>
</div>
</div>
</div>
</section>7. Отзывы
Отзывы — сеть цитат, а не «просто текст под шапкой». Используем figure с blockquote и figcaption: семантика сразу объясняет скринридеру, где цитата, а где автор. Звёзды рисуем символами, аватар — кругом с инициалами: никаких картинок, только стили.
.reviews { padding: 72px 0; background: var(--surface); }
.reviews h2 { text-align: center; margin-bottom: 40px; }
.reviews-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.review { padding: 24px; background: var(--bg); border: 1px solid var(--border); border-radius: 14px; }
.review .stars { margin-bottom: 12px; color: #f5a623; letter-spacing: 2px; font-size: 1.1rem; }
.review .author { display: flex; align-items: center; gap: 12px; margin-top: 16px; }
.review .avatar {
width: 44px; height: 44px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: var(--accent-soft); color: var(--accent); font-weight: 700;
}
.review .author strong { display: block; font-size: .95rem; }
.review .author span { color: var(--text-muted); font-size: .85rem; }<section class="reviews" id="reviews">
<div class="container">
<h2>Что говорят гости и владельцы</h2>
<div class="reviews-grid">
<figure class="review">
<div class="stars">★★★★★</div>
<blockquote>Бронь в три касания, ноль звонков администратору.</blockquote>
<figcaption class="author">
<span class="avatar">АИ</span>
<span><strong>Анна Иванова</strong>управляющая «Кофе на Литейной»</span>
</figcaption>
</figure>
<figure class="review">
<div class="stars">★★★★★</div>
<blockquote>В субботу зал заполнен на 90% — и при этом тихо!</blockquote>
<figcaption class="author">
<span class="avatar">МК</span>
<span><strong>Михаил Ким</strong>владелец «Мясного двора»</span>
</figcaption>
</figure>
<figure class="review">
<div class="stars">★★★★★</div>
<blockquote>Гости всё бронируют в боте, мы экономим час на телефоне.</blockquote>
<figcaption class="author">
<span class="avatar">ЕС</span>
<span><strong>Елена Соколова</strong>администратор «Узелка»</span>
</figcaption>
</figure>
</div>
</div>
</section>aria-label="5 из 5", об этом подробнее в курсе по доступности.8. Форма и кнопка отправки
Заявка — смысл лендинга. Соблюдаем семантику: каждая <label> связана с полем через for и id, обязательные поля помечены required, а типы полей — настоящие (tel, email): браузер сам проверит ввод и покажет клавиатуру нужного типа.
<section class="order" id="order">
<div class="container">
<h2>Подключить бота</h2>
<form class="order-form" action="#" method="post">
<div class="form-group">
<label for="name">Имя</label>
<input type="text" id="name" name="name" required placeholder="Как к вам обращаться">
</div>
<div class="form-group">
<label for="phone">Телефон</label>
<input type="tel" id="phone" name="phone" required placeholder="+7 (999) 000-00-00">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required placeholder="you@mail.ru">
</div>
<div class="form-group">
<label for="message">Сообщение</label>
<textarea id="message" name="message" rows="4" required placeholder="Расскажите о заведении"></textarea>
</div>
<button class="btn btn-primary" type="submit">Отправить заявку</button>
</form>
</div>
</section>.order { padding: 72px 0; }
.order h2 { text-align: center; margin-bottom: 40px; }
.order-form {
max-width: 640px;
margin: 0 auto;
padding: 40px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 14px;
}
.form-group { margin-bottom: 18px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 600; }
.form-group input,
.form-group textarea {
width: 100%;
padding: 12px 14px;
font: inherit;
color: var(--text);
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
}
.form-group input:focus,
.form-group textarea:focus {
border-color: var(--accent);
outline: 2px solid var(--accent-soft); /* мягкая рамка фокуса */
outline-offset: 1px;
}
.form-group input:invalid:not(:placeholder-shown) { border-color: #dc2626; }
.order-form .btn { width: 100%; }
.btn:disabled { opacity: .6; cursor: not-allowed; } /* состояние кнопки при отправке */type ломает не только проверку, но и мобильную клавиатуру: для телефона поле email откроет «@», а tel — цифровую. Меняйте тип осознанно.9. Подвал
Подвал закрывает страницу: логотип для возврата в начало, короткое меню и копирайт. Раскладываем флексом, а между колонок оставляем зазор. Типографика мелкая и приглушённая — подвал не должен спорить с героем.
.site-footer { padding: 32px 0; border-top: 1px solid var(--border); }
.site-footer .container {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 16px;
}
.site-footer p { color: var(--text-muted); font-size: .9rem; }
.footer-nav { display: flex; gap: 20px; font-size: .9rem; }
.footer-nav a:hover { color: var(--accent); }<footer class="site-footer">
<div class="container">
<a class="logo" href="#">🍽 Столик</a>
<nav class="footer-nav">
<a href="#features">Преимущества</a>
<a href="#pricing">Тарифы</a>
<a href="#order">Подключить</a>
</nav>
<p>© 2026 «Столик» · бот для брони столиков в кафе</p>
</div>
</footer>10. Адаптив
Крупные сетки уже адаптивны сами по себе (flex-wrap и auto-fit). Media queries остаются для типов, шрифтов и шапки. Два ключевых рубежа: 900px — планшеты, 600px — телефоны.
/* планшеты и узкие ноутбуки */
@media (max-width: 900px) {
.hero h1 { font-size: 2.1rem; }
.features-grid, .pricing-grid, .reviews-grid { gap: 16px; }
}
/* телефоны */
@media (max-width: 600px) {
.site-header .container { height: 56px; }
.nav { display: none; } /* меню прячем - CTA в герое остаётся */
.hero { padding: 56px 0; }
.hero h1 { font-size: 1.7rem; }
.hero .btn { display: block; max-width: 260px; margin: 0 auto 12px; }
.order-form { padding: 24px; }
.site-footer .container { flex-direction: column; text-align: center; }
}@media (min-width: ...) — для широких экранов. Оба подхода рабочие, главное — зафиксировать один.11. Микроанимации и завершение
Наш лендинг уже собрал скелет, шапку, герой, преимущества, тарифы, отзывы, форму и подвал. Финальный слой — «полировка»: плавные переходы на интерактивных элементах, заметные фокус-контуры и уважение к prefers-reduced-motion.
html { scroll-behavior: smooth; } /* плавный скролл по якорям */
a, button, .feature-card, .plan, .review {
transition: transform .2s ease, box-shadow .2s ease,
color .2s ease, background-color .2s ease;
}
.btn-primary:hover { background: var(--accent-dark); color: #fff; }
.btn-ghost:hover { background: rgba(255, 255, 255, .15); }
/* фокус-контур: видно клавиатурным пользователям */
:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px; }
/* отключение движения по запросу системы */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
}
}Класс :focus-visible показывает контур только когда фокус пришёл с клавиатуры, а не с клика мышкой: клик не портит дизайн, клавиатура не теряет ориентацию. transform и opacity анимируются дёшево — их и ускоряйте, а не margin или width.
scroll-behavior: smooth — и переход по меню выглядит дорого. Но помните про prefers-reduced-motion в блоке выше.12. Проверка и публикация
Лендинг собран. Прежде чем делиться ссылкой, пройдите финальную проверку. Три инструмента закрывают почти все проблемы.
- Валидатор HTML — validator.w3.org найдёт незакрытые теги и ошибки атрибутов;
- Lighthouse — вкладка Audits в DevTools даст баллы по производительности, доступности и SEO;
- Проверка контраста — DevTools: клик на цвет → окошко контрастности подскажет, тянет ли пара цветов текста/фона на 4.5:1.
Куда публиковать статический лендинг:
- GitHub Pages — бесплатно, ссылка вида
username.github.io; - Netlify Drop — перетащили папку, получили ссылку (и HTTPS автоматически).
# GitHub Pages: в настройках репозитория выберите ветку main и сохраните
git init
git add .
git commit -m "Лендинг «Столик»"
git push -u origin mainФинальный чек-лист
- HTML проходит валидатор без ошибок;
- на странице один
<h1>, у каждого поля формы есть<label>; - контраст основного текста ≥ 4.5:1;
- Lighthouse: Performance и Accessibility ≥ 90;
- адаптив проверен на 360, 768 и 1440 px;
- все якоря меню ведут на существующие секции;
- страница открывается по публичной ссылке без ошибок консоли.
--text-muted: #6b7280 на белом фоне близок к границе нормы. Для подписи мельче 14px берите цвет темнее — курс Цвет и контраст разбирает это подробно.Что дальше
Вы прошли весь цикл «с нуля до публикации»: скелет, переменные, шапка, герой, преимущества на flex, тарифы на grid, отзывы, форма, подвал, адаптив, анимации и проверка. Теперь углубляйтесь точечно: правила приоритетов и каскада — в курсе CSS, сетки и их трюки — в Flexbox и Grid, плавные сцены и ключевые кадры — в CSS-анимации, работа со шрифтами — в Веб-типографика, а если где-то застряли в разметке — вернитесь к HTML. Возьмите свой реальный продукт и соберите ему лендинг по этому же плану — это лучший способ закрепить урок.