Основы CSS

CSS (Cascading Style Sheets) — язык каскадных таблиц стилей, который отвечает за внешний вид HTML-документа: цвета, размеры, отступы, расположение элементов, шрифты и анимацию.

CSS-правило состоит из селектора и блока объявлений:

.card {
  color: #222;
  background-color: #fff;
  padding: 16px;
}

Комментарий в CSS:

/* Это комментарий */

Содержание


Подключение стилей

CSS можно подключать к HTML несколькими способами.

Основной и рекомендуемый способ:

<head>
  <link rel="stylesheet" href="styles.css">
</head>

Файл styles.css:

body {
  margin: 0;
  font-family: Arial, sans-serif;
}

Преимущества:

Элемент <style>

CSS размещается непосредственно в HTML-документе:

<head>
  <style>
    body {
      background-color: #f5f5f5;
    }

    h1 {
      color: #2c3e50;
    }
  </style>
</head>

Подходит для небольших страниц, экспериментов и стилей, относящихся только к одному документу.

Инлайн-атрибут style

Стили указываются прямо в HTML-элементе:

<p style="color: red; font-weight: 700;">
  Важное сообщение
</p>

Инлайн-стили имеют высокую специфичность, но их не рекомендуется использовать для основной стилизации:

Правило @import

Один CSS-файл может подключить другой:

@import url("fonts.css");
@import url("layout.css");

body {
  margin: 0;
}

Можно указать условие подключения:

@import url("print.css") print;

Правила @import должны находиться в начале файла, до обычных CSS-правил. Исключением являются некоторые служебные правила, например @charset.

Для основных стилей предпочтительнее использовать <link>, поскольку цепочки @import могут замедлять загрузку.

Порядок подключения

При одинаковой специфичности применяется правило, объявленное позже:

<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="theme.css">
<link rel="stylesheet" href="page.css">

В этом примере theme.css может переопределить base.css, а page.css — оба предыдущих файла.

Типичный порядок:

<link rel="stylesheet" href="normalize.css">
<link rel="stylesheet" href="fonts.css">
<link rel="stylesheet" href="base.css">
<link rel="stylesheet" href="components.css">
<link rel="stylesheet" href="pages.css">

Селекторы

Селектор определяет, к каким HTML-элементам применяются стили.

Селектор по тегу

Выбирает все элементы указанного типа:

p {
  color: #333;
}

button {
  cursor: pointer;
}

Селектор по классу

Класс записывается через точку:

<p class="message">Сообщение</p>
.message {
  padding: 12px;
  background-color: #eef5ff;
}

Один класс можно назначить нескольким элементам:

<p class="note">Первая заметка</p>
<p class="note">Вторая заметка</p>

У элемента может быть несколько классов:

<button class="button button--primary">Сохранить</button>

Селектор по id

Идентификатор записывается через #:

<section id="contacts">...</section>
#contacts {
  background-color: #f5f5f5;
}

Значение id должно быть уникальным в пределах страницы. Для стилизации обычно удобнее использовать классы, поскольку id обладает высокой специфичностью.

Атрибутные селекторы

Выбирают элементы по наличию или значению атрибута:

/* Любой элемент с атрибутом disabled */
[disabled] {
  opacity: 0.5;
}

/* Поле с точным значением атрибута */
input[type="text"] {
  border: 1px solid #aaa;
}

/* Ссылка, адрес которой начинается с https */
a[href^="https"] {
  color: green;
}

/* Ссылка на PDF-файл */
a[href$=".pdf"] {
  font-weight: 700;
}

/* Атрибут содержит указанную подстроку */
[class*="card"] {
  box-sizing: border-box;
}

Группировка селекторов

Одинаковые стили можно назначить нескольким селекторам:

h1,
h2,
h3 {
  font-family: Georgia, serif;
  line-height: 1.2;
}

Комбинаторы

Комбинаторы описывают отношения между элементами.

Комбинатор Название Описание
пробел Потомок Элемент на любом уровне вложенности
> Дочерний элемент Только непосредственный потомок
+ Следующий сосед Первый соседний элемент после выбранного
~ Последующие соседи Все подходящие соседи после выбранного

Потомок — пробел

article p {
  color: #444;
}

Выбирает все <p>, находящиеся внутри <article> на любом уровне вложенности.

Дочерний элемент — >

.menu > li {
  display: inline-block;
}

Выбирает только <li>, которые являются непосредственными дочерними элементами .menu.

Следующий сосед — +

h2 + p {
  margin-top: 0;
}

Выбирает первый <p>, расположенный сразу после <h2>.

Последующие соседи — ~

h2 ~ p {
  color: #666;
}

Выбирает все <p>, расположенные после <h2> и имеющие с ним общего родителя.


Псевдоклассы и псевдоэлементы

Псевдоклассы

Псевдокласс выбирает элемент с учётом его состояния или положения.

:hover

Применяется при наведении указателя:

.button:hover {
  background-color: #174ea6;
}

На сенсорных устройствах наведение может отсутствовать, поэтому важные действия не должны зависеть только от :hover.

:focus

Выбирает элемент, получивший фокус:

input:focus {
  border-color: #2563eb;
  outline: 2px solid #93c5fd;
}

Не рекомендуется без замены отключать outline, поскольку он помогает пользователям клавиатуры видеть текущий элемент.

:focus-visible

Обычно показывает фокус только тогда, когда браузер считает его необходимым, например при навигации клавиатурой:

.button:focus {
  outline: none;
}

.button:focus-visible {
  outline: 3px solid #f59e0b;
  outline-offset: 3px;
}

:nth-child()

Выбирает элементы по их позиции среди всех дочерних элементов родителя:

/* Второй дочерний элемент */
li:nth-child(2) {
  font-weight: 700;
}

/* Нечётные элементы */
tr:nth-child(odd) {
  background-color: #f5f5f5;
}

/* Чётные элементы */
tr:nth-child(even) {
  background-color: #fff;
}

/* Каждый третий элемент */
li:nth-child(3n) {
  color: red;
}

/* Первые три элемента */
li:nth-child(-n + 3) {
  font-weight: 700;
}

:not()

Исключает элементы, соответствующие указанному селектору:

button:not(.button--primary) {
  background-color: #ddd;
}

Несколько условий:

input:not([type="checkbox"]):not([type="radio"]) {
  width: 100%;
}

Псевдоэлементы

Псевдоэлемент стилизует условную часть элемента или создаёт декоративный фрагмент.

::before и ::after

Создают псевдоэлемент до или после содержимого:

.link::after {
  content: " →";
}

.required::after {
  content: " *";
  color: red;
}

Для ::before и ::after обычно необходимо свойство content:

.card::before {
  content: "";
  display: block;
  width: 40px;
  height: 4px;
  background-color: #2563eb;
}

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


Каскад, специфичность и наследование

Каскад

Если к элементу подходят несколько правил, браузер определяет победителя с учётом:

  1. важности объявления;
  2. источника стилей;
  3. специфичности селектора;
  4. порядка правил.

При одинаковой специфичности побеждает правило, записанное позже:

.message {
  color: blue;
}

.message {
  color: red;
}

Текст будет красным.

Специфичность

Специфичность можно условно записывать четырьмя группами:

инлайн-стили | id | классы, атрибуты, псевдоклассы | теги, псевдоэлементы
Селектор Условный вес
p 0-0-0-1
p::before 0-0-0-2
.message 0-0-1-0
[type="text"] 0-0-1-0
button:hover 0-0-1-1
#header 0-1-0-0
style="" 1-0-0-0

Пример:

p {
  color: black; /* 0-0-0-1 */
}

.article p {
  color: blue; /* 0-0-1-1 */
}

#content .article p {
  color: red; /* 0-1-1-1 */
}

Если элемент находится внутри #content и .article, победит последнее правило.

Комбинаторы >, +, ~ и пробел не добавляют специфичность.

!important

!important повышает важность объявления:

.message {
  color: red !important;
}

Переопределить его обычным правилом нельзя. Среди нескольких !important-объявлений снова учитываются каскад и специфичность.

Частое применение !important усложняет поддержку CSS. Обычно лучше исправить порядок подключения или селектор.

Наследование

Некоторые свойства передаются дочерним элементам:

body {
  color: #333;
  font-family: Arial, sans-serif;
}

Текст внутри body обычно унаследует цвет и шрифт.

Часто наследуются:

Обычно не наследуются:

inherit, initial, unset

.element {
  color: inherit;
}

inherit принудительно наследует значение от родителя.

.element {
  color: initial;
}

initial устанавливает начальное значение свойства согласно спецификации CSS.

.element {
  color: unset;
}

unset работает как inherit для наследуемых свойств и как initial для ненаследуемых.


Бокс-модель

Каждый HTML-элемент рассматривается как прямоугольный блок.

Бокс-модель состоит из:

  1. content — содержимое;
  2. padding — внутренний отступ;
  3. border — граница;
  4. margin — внешний отступ.
.card {
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
  margin: 16px;
}

При стандартном box-sizing: content-box указанная ширина относится только к содержимому:

300px + 40px padding + 4px border = 344px

Внешние отступы margin в размер элемента не входят.

box-sizing: border-box

При border-box свойства width и height включают содержимое, внутренние отступы и границы:

.card {
  box-sizing: border-box;
  width: 300px;
  padding: 20px;
  border: 2px solid #333;
}

Итоговая ширина останется 300px.

Обычно border-box включают для всех элементов:

*,
*::before,
*::after {
  box-sizing: border-box;
}

Схлопывание внешних отступов

Вертикальные margin блочных элементов в обычном потоке могут объединяться:

.first {
  margin-bottom: 30px;
}

.second {
  margin-top: 20px;
}

Расстояние между элементами обычно будет 30px, а не 50px.

Схлопывание может происходить:

Отступы не схлопываются во Flexbox и Grid. Схлопывание также можно предотвратить, например, с помощью внутреннего отступа, границы или нового контекста форматирования:

.parent {
  display: flow-root;
}

Единицы измерения

Единица Значение
px CSS-пиксель
% Процент от значения, определяемого свойством
em Относительно размера шрифта текущего элемента
rem Относительно размера шрифта корневого элемента
vw Один процент ширины области просмотра
vh Один процент высоты области просмотра
ch Приблизительная ширина символа 0 текущего шрифта
fr Доля свободного пространства в CSS Grid

px

Подходит для границ, небольших отступов и точных размеров:

.card {
  border: 1px solid #ddd;
  border-radius: 8px;
}

%

Зависит от контекста:

.image {
  width: 100%;
}

Процентная ширина обычно рассчитывается от ширины содержащего блока.

em

Зависит от вычисленного размера шрифта элемента:

.button {
  font-size: 16px;
  padding: 0.75em 1.5em;
}

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

При вложенности em может накапливаться:

.list {
  font-size: 1.2em;
}

Если списки вложены друг в друга, размер текста может увеличиваться на каждом уровне.

rem

Зависит от размера шрифта корневого <html>:

html {
  font-size: 16px;
}

h1 {
  font-size: 2rem; /* 32px */
}

Удобен для согласованной системы размеров и отступов.

vw и vh

.hero {
  min-height: 100vh;
}

.title {
  font-size: 5vw;
}

Для текста обычно следует ограничивать масштабирование:

.title {
  font-size: clamp(2rem, 5vw, 4rem);
}

ch

Удобен для ограничения длины текстовой строки:

.article {
  max-width: 65ch;
}

fr

Применяется в CSS Grid:

.layout {
  display: grid;
  grid-template-columns: 1fr 2fr;
}

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

Критерии выбора


Цвет и фон

Шестнадцатеричная запись

.element {
  color: #2563eb;
  background-color: #fff;
}

Краткая форма:

.element {
  color: #333; /* #333333 */
}

Альфа-канал:

.element {
  background-color: #00000080;
}

rgb()

.element {
  color: rgb(37, 99, 235);
}

С прозрачностью:

.overlay {
  background-color: rgb(0 0 0 / 50%);
}

hsl()

HSL описывает цвет через тон, насыщенность и светлоту:

.element {
  color: hsl(217 91% 60%);
}

С прозрачностью:

.element {
  background-color: hsl(217 91% 60% / 30%);
}

Альфа-канал

Альфа-канал определяет прозрачность:

Прозрачный цвет и opacity работают по-разному:

.card {
  opacity: 0.5;
}

opacity делает полупрозрачным весь элемент вместе с его содержимым.

.card {
  background-color: rgb(0 0 0 / 50%);
}

Здесь полупрозрачным будет только фон.

Фоновое изображение

.hero {
  background-image: url("images/hero.jpg");
  background-repeat: no-repeat;
  background-position: center;
  background-size: cover;
}

background-size

.element {
  background-size: auto;
  background-size: cover;
  background-size: contain;
  background-size: 300px 200px;
}

background-position

.element {
  background-position: center;
  background-position: top right;
  background-position: 50% 25%;
}

linear-gradient()

.banner {
  background-image: linear-gradient(
    135deg,
    #2563eb,
    #7c3aed
  );
}

Полупрозрачный слой поверх изображения:

.hero {
  background-image:
    linear-gradient(rgb(0 0 0 / 60%), rgb(0 0 0 / 60%)),
    url("images/hero.jpg");
  background-position: center;
  background-size: cover;
}

Типографика

font-family

body {
  font-family: Arial, sans-serif;
}

Если имя шрифта содержит пробелы, оно заключается в кавычки:

body {
  font-family: "Open Sans", sans-serif;
}

Стек шрифтов

Браузер последовательно ищет доступный шрифт:

body {
  font-family:
    Inter,
    Arial,
    Helvetica,
    sans-serif;
}

В конце рекомендуется указывать обобщённое семейство:

Системный стек:

body {
  font-family:
    system-ui,
    -apple-system,
    "Segoe UI",
    sans-serif;
}

Основные свойства

.text {
  font-size: 1rem;
  line-height: 1.5;
  font-weight: 400;
  letter-spacing: 0.01em;
}
Свойство Назначение
font-size Размер шрифта
line-height Высота строки
font-weight Насыщенность шрифта
letter-spacing Расстояние между символами

Для основного текста часто используют безразмерный line-height:

body {
  line-height: 1.5;
}

Он вычисляется относительно размера шрифта каждого элемента.

Типичные значения font-weight:

.text {
  font-weight: 400;
}

.title {
  font-weight: 700;
}

Доступные значения зависят от подключённого шрифта.

@font-face

Позволяет подключить локальный файл шрифта:

@font-face {
  font-family: "Inter";
  src: url("fonts/inter-regular.woff2") format("woff2");
  font-style: normal;
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: "Inter";
  src: url("fonts/inter-bold.woff2") format("woff2");
  font-style: normal;
  font-weight: 700;
  font-display: swap;
}

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

body {
  font-family: "Inter", Arial, sans-serif;
}

font-display: swap разрешает временно показывать текст системным шрифтом, пока загружается основной.


display и поток документа

В обычном потоке элементы располагаются в порядке, в котором они записаны в HTML.

display: block

Блочный элемент обычно занимает всю доступную ширину и начинается с новой строки:

section {
  display: block;
}

Для блочных элементов работают width, height, вертикальные margin и padding.

display: inline

Строчный элемент располагается внутри строки:

span {
  display: inline;
}

Для обычных строчных элементов width и height не задают размер так же, как у блоков.

display: inline-block

Элемент располагается в строке, но принимает размеры и отступы как блок:

.badge {
  display: inline-block;
  padding: 4px 8px;
  width: 120px;
}

display: none

Полностью удаляет элемент из визуального потока:

.hidden {
  display: none;
}

Элемент не занимает место и обычно недоступен для вспомогательных технологий.

visibility: hidden

Скрывает элемент, но сохраняет занимаемое им место:

.invisible {
  visibility: hidden;
}

Схлопывание пробелов

В обычном HTML несколько пробелов и переносов строк отображаются как один пробел:

<p>
  Один       два
  три
</p>

На странице текст обычно будет выглядеть так:

Один два три

Поведение регулирует white-space:

.text {
  white-space: normal;
}

.code {
  white-space: pre;
}

.no-wrap {
  white-space: nowrap;
}

.multiline {
  white-space: pre-wrap;
}

Позиционирование

Свойство position определяет способ размещения элемента.

static

Значение по умолчанию:

.element {
  position: static;
}

Свойства top, right, bottom и left обычно не влияют на статический элемент.

relative

Элемент остаётся в обычном потоке, но может быть визуально смещён:

.element {
  position: relative;
  top: 10px;
  left: 20px;
}

Исходное место элемента сохраняется.

position: relative также часто задаёт систему координат для абсолютно позиционированных потомков.

absolute

Элемент исключается из обычного потока:

.card {
  position: relative;
}

.card__badge {
  position: absolute;
  top: 8px;
  right: 8px;
}

Абсолютный элемент позиционируется относительно ближайшего предка, у которого position отличается от static. Если такого предка нет, позиционирование выполняется относительно начального содержащего блока.

fixed

Элемент исключается из потока и обычно фиксируется относительно окна браузера:

.chat-button {
  position: fixed;
  right: 24px;
  bottom: 24px;
}

Он остаётся на месте при прокрутке страницы.

sticky

Элемент ведёт себя как обычный до достижения указанной границы, после чего временно фиксируется:

.header {
  position: sticky;
  top: 0;
  background-color: white;
}

Для работы sticky необходимо указать хотя бы одну границу, например top: 0. На поведение также влияет прокручиваемый контейнер и его overflow.

inset

Краткая запись для координат:

.overlay {
  position: absolute;
  inset: 0;
}

Эквивалентно:

.overlay {
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

z-index

Управляет порядком наложения элементов:

.modal {
  position: fixed;
  z-index: 1000;
}

Элемент с большим z-index обычно отображается поверх элемента с меньшим значением, если они находятся в сопоставимых контекстах наложения.

Стекинг-контекст

Стекинг-контекст — независимая область управления слоями. z-index дочернего элемента не может вывести его за пределы контекста родителя.

Новый стекинг-контекст могут создавать:

.parent {
  position: relative;
  z-index: 1;
}

.child {
  position: absolute;
  z-index: 9999;
}

Даже большой z-index дочернего элемента действует внутри контекста .parent.


Размеры и переполнение

width и height

.card {
  width: 320px;
  height: 200px;
}

Для адаптивных элементов часто используют:

.card {
  width: 100%;
  height: auto;
}

Фиксированная высота для текстовых блоков может привести к переполнению, если содержимое увеличится.

Минимальные и максимальные размеры

.container {
  width: 100%;
  max-width: 1200px;
  min-width: 320px;
}
.panel {
  min-height: 200px;
  max-height: 600px;
}

Типичный контейнер страницы:

.container {
  width: min(100% - 32px, 1200px);
  margin-inline: auto;
}

overflow

Определяет поведение содержимого, которое не помещается:

.element {
  overflow: visible;
  overflow: hidden;
  overflow: auto;
  overflow: scroll;
}

Можно управлять осями отдельно:

.table-wrapper {
  overflow-x: auto;
  overflow-y: hidden;
}

object-fit

Определяет, как изображение или видео помещается в заданную область:

.avatar {
  width: 120px;
  height: 120px;
  object-fit: cover;
}
Значение Поведение
fill Растягивает содержимое без сохранения пропорций
contain Полностью помещает содержимое
cover Заполняет область с возможной обрезкой
none Сохраняет исходный размер
scale-down Выбирает меньший вариант между none и contain

Положение содержимого задаёт object-position:

.photo {
  object-fit: cover;
  object-position: center top;
}

aspect-ratio

Задаёт соотношение сторон:

.video {
  width: 100%;
  aspect-ratio: 16 / 9;
}

Квадратная карточка:

.square {
  aspect-ratio: 1;
}

Пример адаптивного изображения:

.card__image {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

Кастомные свойства и организация CSS

Кастомные свойства

CSS-переменные объявляются с префиксом --:

:root {
  --color-primary: #2563eb;
  --color-text: #1f2937;
  --spacing-medium: 16px;
  --border-radius: 8px;
}

Использование через var():

.button {
  color: white;
  background-color: var(--color-primary);
  padding: var(--spacing-medium);
  border-radius: var(--border-radius);
}

Резервное значение

.element {
  color: var(--element-color, #333);
}

Если --element-color не определена, будет использовано значение #333.

Область видимости

Кастомные свойства участвуют в наследовании:

:root {
  --accent: blue;
}

.warning {
  --accent: red;
}

.warning__title {
  color: var(--accent);
}

Внутри .warning переменная --accent будет равна red.

БЭМ-именование

БЭМ — методология именования CSS-классов: блок, элемент, модификатор.

<article class="card card--featured">
  <img class="card__image" src="photo.jpg" alt="">
  <h2 class="card__title">Заголовок</h2>
  <p class="card__text">Описание карточки</p>
</article>
.card {
  padding: 16px;
}

.card__image {
  width: 100%;
}

.card__title {
  font-size: 1.5rem;
}

.card--featured {
  border: 2px solid #2563eb;
}

Не рекомендуется привязывать БЭМ-классы к структуре тегов:

/* Менее устойчивый вариант */
.card article h2 {
  color: blue;
}

/* Более устойчивый вариант */
.card__title {
  color: blue;
}

Reset CSS

CSS Reset удаляет или переопределяет большую часть стандартных браузерных стилей:

* {
  margin: 0;
  padding: 0;
}

ul,
ol {
  list-style: none;
}

button,
input,
textarea {
  font: inherit;
}

Полный сброс требует последующего явного оформления элементов.

Normalize.css

normalize.css не удаляет все стили, а приводит браузерные значения к более единообразному поведению.

Различие:

Минимальная базовая настройка:

*,
*::before,
*::after {
  box-sizing: border-box;
}

html {
  scroll-behavior: smooth;
}

body {
  margin: 0;
  min-height: 100vh;
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

img,
picture,
video {
  display: block;
  max-width: 100%;
}

button,
input,
textarea,
select {
  font: inherit;
}

Переходы и анимация

transition

Плавно изменяет значение CSS-свойства:

.button {
  background-color: #2563eb;
  transform: scale(1);
  transition:
    background-color 200ms ease,
    transform 200ms ease;
}

.button:hover {
  background-color: #1d4ed8;
  transform: scale(1.05);
}

Основные составляющие:

.element {
  transition-property: transform;
  transition-duration: 300ms;
  transition-timing-function: ease;
  transition-delay: 0ms;
}

Краткая запись:

.element {
  transition: transform 300ms ease 0ms;
}

Не все CSS-свойства можно плавно анимировать. Например, переход от display: none к display: block обычным transition не выполняется.

Не рекомендуется без необходимости писать:

.element {
  transition: all 300ms ease;
}

Лучше явно перечислять изменяемые свойства.

transform

Трансформации меняют визуальное представление элемента, обычно не влияя на расположение соседей.

translate()

.element {
  transform: translate(20px, 10px);
}

Отдельно по осям:

.element {
  transform: translateX(20px);
  transform: translateY(-10px);
}

Центрирование абсолютно позиционированного элемента:

.element {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

scale()

.element {
  transform: scale(1.1);
}

Значение больше 1 увеличивает элемент, а меньше 1 — уменьшает.

Несколько трансформаций можно объединять:

.element {
  transform: translateY(-4px) scale(1.05);
}

Порядок функций имеет значение.

@keyframes

Определяет этапы анимации:

@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(10px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

То же самое в процентах:

@keyframes pulse {
  0% {
    transform: scale(1);
  }

  50% {
    transform: scale(1.1);
  }

  100% {
    transform: scale(1);
  }
}

animation

Применяет созданную анимацию:

.notification {
  animation: fade-in 400ms ease-out;
}

Полная запись:

.element {
  animation-name: pulse;
  animation-duration: 1s;
  animation-timing-function: ease-in-out;
  animation-delay: 0s;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  animation-fill-mode: both;
  animation-play-state: running;
}

Краткая запись:

.element {
  animation: pulse 1s ease-in-out 0s infinite alternate both;
}

Отключение анимации по настройкам пользователя

Пользователь может включить уменьшение движения в настройках системы:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Общий пример

Файл index.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="stylesheet" href="styles.css">
</head>
<body>
  <main class="page">
    <article class="card card--featured">
      <span class="card__badge">Новинка</span>

      <img
        class="card__image"
        src="product.jpg"
        alt="Описание товара"
      >

      <div class="card__content">
        <h1 class="card__title">Название товара</h1>
        <p class="card__description">
          Краткое описание товара с его основными преимуществами.
        </p>
        <button class="button" type="button">
          Добавить в корзину
        </button>
      </div>
    </article>
  </main>
</body>
</html>

Файл styles.css:

:root {
  --color-primary: #2563eb;
  --color-primary-hover: #1d4ed8;
  --color-text: #1f2937;
  --color-surface: #fff;
  --color-background: #f3f4f6;
  --radius: 12px;
  --shadow: 0 10px 25px rgb(0 0 0 / 10%);
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

body {
  margin: 0;
  color: var(--color-text);
  background-color: var(--color-background);
  font-family: system-ui, sans-serif;
  line-height: 1.5;
}

.page {
  display: grid;
  min-height: 100vh;
  padding: 24px;
  place-items: center;
}

.card {
  position: relative;
  width: min(100%, 360px);
  overflow: hidden;
  background-color: var(--color-surface);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

.card__badge {
  position: absolute;
  z-index: 1;
  top: 12px;
  right: 12px;
  padding: 4px 8px;
  color: white;
  background-color: var(--color-primary);
  border-radius: 999px;
  font-size: 0.875rem;
}

.card__image {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

.card__content {
  padding: 20px;
}

.card__title {
  margin: 0 0 8px;
  font-size: 1.5rem;
  line-height: 1.2;
}

.card__description {
  margin: 0 0 20px;
  color: #4b5563;
}

.button {
  display: inline-block;
  width: 100%;
  padding: 12px 20px;
  color: white;
  background-color: var(--color-primary);
  border: 0;
  border-radius: 8px;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  transition:
    background-color 200ms ease,
    transform 200ms ease;
}

.button:hover {
  background-color: var(--color-primary-hover);
  transform: translateY(-2px);
}

.button:focus-visible {
  outline: 3px solid #93c5fd;
  outline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .button {
    transition: none;
  }
}