Стили и classList
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
style — карандаш, которым дописывают одну строчку поверх готовой вёрстки, а classList — переключатель между заранее написанными сценариями. Выбор между ними решает, останется ли внешний вид страницы в CSS или расползётся по коду.
Свойство style — это не «все стили элемента», а ровно тот текст, который лежит в его атрибуте style. Отсюда два следствия: имена пишутся в camelCase (background-color становится backgroundColor), а значение — всегда строка с единицами измерения. Число без px браузер молча выбросит.
Записанное так правило перебивает почти всё, что скажет таблица стилей: инлайн-стиль сильнее любого селектора. Убрать его можно присвоением пустой строки — свойство исчезнет из атрибута, и элемент вернётся к тому, что описано в CSS. Именно поэтому style хорош для точечных правок: подставить посчитанную ширину, сдвинуть элемент на нужное число пикселей, показать процент прогресса.
Если цвет пришёл из CSS-файла, element.style.color вернёт пустую строку — в атрибуте ведь ничего нет. Итоговое, реально применённое значение показывает getComputedStyle(element): браузер прогоняет все правила, наследование и единицы и отдаёт результат в пикселях и rgb().
Объект от getComputedStyle — только для чтения, и попытка записи не проходит молча, а бросает исключение: объект описывает, что получилось, а не что вы хотите. Меняют по-прежнему style или класс. Чем вычисленный стиль отличается от заданного, почему длины приходят пикселями и что показывает скрытый элемент — в следующей статье.
className — обычная строка, и добавить к ней класс, не сломав остальные, неудобно. classList — набор с понятными методами: add, remove, contains, toggle. add и remove принимают сколько угодно имён сразу, дубликаты не создаются, а удаление несуществующего класса не ошибка. А вот contains берёт ровно одно имя, и у toggle второй аргумент — не второе имя, а положение переключателя: toggle("open", "wide") не тронет wide, а намертво включит open.
toggle отличается тем, что возвращает результат: true, если класс в итоге на месте. А второй аргумент превращает переключатель в выключатель с заданным положением: toggle("open", isOpen) добавит класс при true и снимет при false. Это надёжнее, чем if с двумя ветками, потому что состояние класса выводится из данных, а не накапливается вручную.
Инлайн-стиль описывает внешний вид там же, где живёт логика, — и внешний вид перестаёт быть переиспользуемым. Класс, наоборот, это имя состояния: JS говорит «эта карточка выбрана», а как выглядит выбранная карточка и плавно ли она в этот вид переходит, решает CSS. Дизайнер меняет вид, не открывая ваш код.
| Задача | style | Класс |
|---|---|---|
| Покрасить один элемент по расчёту | подходит | на каждое значение нужен новый класс |
| Одинаково оформить сотню элементов | цикл и сотня атрибутов | одно правило в CSS |
| :hover, медиазапросы, анимации | недоступны | работают |
| Вернуть как было | помнить прежнее значение | classList.remove() |
| Найти, откуда взялся вид | искать по всему JS | искать по CSS |
Есть и пятый метод — replace: он меняет один класс на другой и возвращает false, если менять было нечего. Такой ответ — удобный признак того, что элемент оказался не в том состоянии, которое вы ожидали.
У style есть свойство cssText — весь атрибут одной строкой. Присваивание в него выглядит компактно, но переписывает атрибут целиком, стирая всё, что там уже было, включая правила, которые поставил другой участок кода.
Правило, которое экономит часы: если состояние можно назвать словом — открыт, выбран, загружается, — это класс. Инлайн-стиль оставьте для чисел, посчитанных прямо сейчас: координат, ширины, процента прогресса.
Тёмная тема целой страницы — это один класс на корневом элементе. CSS заранее описывает оба варианта селекторами вроде .theme-dark .card, а JS остаётся одна строка. В настоящем проекте корень — это document.documentElement (тег <html>) и выбор темы обычно кладут в localStorage; в примере ниже роль корня играет обычный контейнер.
Второй приём того же семейства — CSS-переменные. Их нельзя записать как обычное свойство: style.someName знает только стандартные имена, а кастомные ставятся через setProperty и читаются через getPropertyValue. Одна переменная на корне перекрашивает всё, что на неё ссылается.
classList.toggle("theme-dark") и root.dataset.theme = "dark" решают одну задачу. Класс короче и привычнее, зато у него только два положения: есть или нет. Data-атрибут держит значение — light, dark, sepia — и в CSS выбирается селектором [data-theme="dark"].
Практическое правило: два состояния — класс, три и больше — data-атрибут. Смешивать оба способа для одной сущности не стоит, иначе у темы окажется два источника правды и когда-нибудь они разойдутся.
- element.style — это атрибут элемента, а не итоговые стили: camelCase в именах, строки с единицами в значениях, пустая строка удаляет свойство.
- Реально применённое значение показывает getComputedStyle(element), и этот объект только для чтения.
- classList даёт add, remove, contains, toggle и replace; toggle возвращает true, если класс остался, а второй аргумент задаёт положение явно.
- Классы держат внешний вид в CSS: работают :hover, медиазапросы и анимации, а откат состояния — одна строка remove.
- Тема страницы — класс или data-атрибут на корневом элементе; переменные меняются через style.setProperty("--accent", ...).
1/3Что окажется в el.style.fontSize после строки el.style.fontSize = 24;?
2/3Элемент не имеет класса open. Что вернёт el.classList.toggle("open") и что произойдёт с классом?
3/3У элемента фон задан правилом из CSS-файла. Что вернёт el.style.backgroundColor?
Переключатель темы, который не путается
Напишите функцию setTheme(el, theme): она держит на элементе класс theme-dark в соответствии с темой и записывает саму тему в data-theme. Функция должна выдерживать повторный вызов с той же темой — состояние не должно «моргать».
Ключ — второй аргумент toggle: он задаёт положение переключателя явно, поэтому повторный вызов с той же темой ничего не меняет.
Вариант с if и парой add/remove тоже рабочий, но длиннее и оставляет шанс забыть одну ветку.
Класс empty у пустого поля
Подпишитесь на событие input и держите класс empty ровно тогда, когда поле пустое. Событие в примере вызывается кодом через dispatchEvent, ждать пользователя не нужно.
Одна подписка и один toggle со вторым аргументом: класс всегда выводится из значения поля, а не накапливается вручную.
Обратите внимание, что цвет рамки здесь не задаётся из JS вовсе — за него отвечает правило .field.empty в CSS.