← JavaScript/Стили и классы в DOM#105 из 383← ПредыдущийСледующий →+25 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

Стили и классы в DOM

В Telegram Web при получении нового сообщения кнопка «Прокрутить вниз» появляется с анимацией — JavaScript добавляет CSS-класс. В Figma при выборе объекта панель свойств подсвечивается. В интернет-магазинах кнопка «В корзину» меняет вид при добавлении товара. Всё это — управление классами и стилями через JavaScript.

Какую проблему решает

Статичный HTML не может реагировать на действия пользователя. JavaScript нужен способ динамически менять внешний вид элементов: добавлять/убирать классы, менять инлайновые стили, переключать темы.

На основе предыдущих уроков

  • DOM — что такое DOM, получение элементов
  • События — обработка кликов и взаимодействий
  • Map/Set — Set как основа для classList
  • classList — работа с классами

    Свойство classList предоставляет удобный API для управления CSS-классами:

    element.classList.add('active')           // добавить класс
    element.classList.remove('active')        // убрать класс
    element.classList.toggle('active')        // переключить
    element.classList.toggle('open', true)    // принудительно добавить (force=true)
    element.classList.toggle('open', false)   // принудительно убрать (force=false)
    element.classList.contains('active')      // true/false
    element.classList.replace('btn-old', 'btn-new')  // заменить

    className vs classList

    // className — строка, перезаписывает все классы
    element.className = 'btn btn-primary'  // заменяет ВСЕ классы
    
    // classList — точечное управление (предпочтительно)
    element.classList.add('btn-primary')   // добавляет к существующим

    Инлайновые стили: element.style

    Для значений, которые нельзя задать CSS-классами (динамические координаты, цвета из API):

    element.style.color = 'red'
    element.style.backgroundColor = 'blue'   // camelCase вместо kebab-case
    element.style.fontSize = '16px'          // значение — строка с единицей!
    element.style.transform = 'translateX(100px)'
    element.style.display = 'none'           // скрыть элемент
    element.style.display = ''              // вернуть к CSS-значению (сбросить инлайн)

    getComputedStyle — итоговые стили

    element.style показывает только инлайновые стили. Чтобы получить финальное значение (с учётом CSS-файлов и браузерных стилей) используй getComputedStyle:

    const styles = getComputedStyle(element)
    styles.color        // 'rgb(255, 0, 0)'
    styles.fontSize     // '16px'
    styles.display      // 'block'

    data-* атрибуты и dataset

    data-* атрибуты хранят данные прямо в HTML-элементах:

    // HTML: <button data-product-id="42" data-action="add-to-cart">Купить</button>
    btn.dataset.productId  // '42' — всегда строка!
    btn.dataset.action     // 'add-to-cart'
    btn.dataset.newProp = 'value'  // добавит data-new-prop

    Правильный паттерн: классы в CSS, логика в JS

    // CSS: .modal { opacity: 0; transition: opacity 0.3s }
    //      .modal.visible { opacity: 1; pointer-events: auto }
    
    // JS: только переключает класс, не трогает стили
    function showModal(modal) { modal.classList.add('visible') }
    function hideModal(modal) { modal.classList.remove('visible') }

    Это лучше чем менять стили напрямую: CSS отвечает за оформление, JS — за логику состояний.

    Типичные ошибки

    Ошибка 1: className затирает все существующие классы

    // Элемент имеет: class="btn btn-large"
    element.className = 'active'  // Теперь только 'active' — потеряли btn и btn-large!
    
    // Правильно
    element.classList.add('active')  // Добавляет к существующим классам

    Ошибка 2: забыли единицу в style

    element.style.width = 100       // не работает! нет 'px'
    element.style.width = '100px'   // правильно
    element.style.fontSize = 16     // не работает
    element.style.fontSize = '16px' // правильно

    Ошибка 3: читают style вместо getComputedStyle

    // Если стиль задан через CSS-файл, а не инлайн:
    element.style.color  // '' — пустая строка! не видит CSS-файлы
    
    // Правильно для чтения итогового значения
    getComputedStyle(element).color  // 'rgb(33, 150, 243)'

    В реальных проектах

  • Тёмная тема: переключение CSS-класса dark на body или через CSS-переменные
  • Skeleton-экраны: класс loading пока данные загружаются
  • Тосты/уведомления: добавление/удаление класса visible с CSS-анимацией
  • Drag and Drop: инлайновые стили для позиционирования перетаскиваемого элемента
  • Примеры

    Симуляция classList API через Set и работа с dataset

    // Симуляция element.classList через Set
    function createMockElement(tag, initialClasses) {
      if (initialClasses === undefined) initialClasses = []
      const classSet = new Set(initialClasses)
      const dataset = {}
    
      return {
        tag,
        dataset,
        classList: {
          add(...classes) {
            classes.forEach(c => classSet.add(c))
          },
          remove(...classes) {
            classes.forEach(c => classSet.delete(c))
          },
          toggle(cls, force) {
            if (force !== undefined) {
              force ? classSet.add(cls) : classSet.delete(cls)
            } else {
              classSet.has(cls) ? classSet.delete(cls) : classSet.add(cls)
            }
            return classSet.has(cls)
          },
          contains(cls) { return classSet.has(cls) },
          replace(oldCls, newCls) {
            if (classSet.has(oldCls)) {
              classSet.delete(oldCls)
              classSet.add(newCls)
              return true
            }
            return false
          },
          get value() { return [...classSet].join(' ') }
        }
      }
    }
    
    // Кнопка добавления в корзину
    const btn = createMockElement('button', ['btn', 'btn-primary'])
    console.log(btn.classList.value)              // 'btn btn-primary'
    
    // Добавляем в корзину — меняем вид кнопки
    btn.classList.replace('btn-primary', 'btn-success')
    btn.classList.add('btn--added')
    console.log(btn.classList.value)              // 'btn btn-success btn--added'
    console.log(btn.classList.contains('btn-success'))  // true
    
    // dataset — хранение данных в элементе
    btn.dataset.productId = '42'
    btn.dataset.quantity = '1'
    console.log(btn.dataset.productId)            // '42'
    
    // Skeleton-экран: переключение состояния загрузки
    const card = createMockElement('div', ['card'])
    card.classList.add('loading')
    console.log(card.classList.contains('loading'))  // true
    card.classList.remove('loading')
    card.classList.add('loaded')
    console.log(card.classList.value)             // 'card loaded'
    
    // toggle с force — принудительное состояние (открыть/закрыть меню)
    const menu = createMockElement('nav', ['menu'])
    menu.classList.toggle('open', true)           // открыть
    console.log(menu.classList.contains('open'))  // true
    menu.classList.toggle('open', false)          // закрыть
    console.log(menu.classList.contains('open'))  // false

    Стили и классы в DOM

    В Telegram Web при получении нового сообщения кнопка «Прокрутить вниз» появляется с анимацией — JavaScript добавляет CSS-класс. В Figma при выборе объекта панель свойств подсвечивается. В интернет-магазинах кнопка «В корзину» меняет вид при добавлении товара. Всё это — управление классами и стилями через JavaScript.

    Какую проблему решает

    Статичный HTML не может реагировать на действия пользователя. JavaScript нужен способ динамически менять внешний вид элементов: добавлять/убирать классы, менять инлайновые стили, переключать темы.

    На основе предыдущих уроков

  • DOM — что такое DOM, получение элементов
  • События — обработка кликов и взаимодействий
  • Map/Set — Set как основа для classList
  • classList — работа с классами

    Свойство classList предоставляет удобный API для управления CSS-классами:

    element.classList.add('active')           // добавить класс
    element.classList.remove('active')        // убрать класс
    element.classList.toggle('active')        // переключить
    element.classList.toggle('open', true)    // принудительно добавить (force=true)
    element.classList.toggle('open', false)   // принудительно убрать (force=false)
    element.classList.contains('active')      // true/false
    element.classList.replace('btn-old', 'btn-new')  // заменить

    className vs classList

    // className — строка, перезаписывает все классы
    element.className = 'btn btn-primary'  // заменяет ВСЕ классы
    
    // classList — точечное управление (предпочтительно)
    element.classList.add('btn-primary')   // добавляет к существующим

    Инлайновые стили: element.style

    Для значений, которые нельзя задать CSS-классами (динамические координаты, цвета из API):

    element.style.color = 'red'
    element.style.backgroundColor = 'blue'   // camelCase вместо kebab-case
    element.style.fontSize = '16px'          // значение — строка с единицей!
    element.style.transform = 'translateX(100px)'
    element.style.display = 'none'           // скрыть элемент
    element.style.display = ''              // вернуть к CSS-значению (сбросить инлайн)

    getComputedStyle — итоговые стили

    element.style показывает только инлайновые стили. Чтобы получить финальное значение (с учётом CSS-файлов и браузерных стилей) используй getComputedStyle:

    const styles = getComputedStyle(element)
    styles.color        // 'rgb(255, 0, 0)'
    styles.fontSize     // '16px'
    styles.display      // 'block'

    data-* атрибуты и dataset

    data-* атрибуты хранят данные прямо в HTML-элементах:

    // HTML: <button data-product-id="42" data-action="add-to-cart">Купить</button>
    btn.dataset.productId  // '42' — всегда строка!
    btn.dataset.action     // 'add-to-cart'
    btn.dataset.newProp = 'value'  // добавит data-new-prop

    Правильный паттерн: классы в CSS, логика в JS

    // CSS: .modal { opacity: 0; transition: opacity 0.3s }
    //      .modal.visible { opacity: 1; pointer-events: auto }
    
    // JS: только переключает класс, не трогает стили
    function showModal(modal) { modal.classList.add('visible') }
    function hideModal(modal) { modal.classList.remove('visible') }

    Это лучше чем менять стили напрямую: CSS отвечает за оформление, JS — за логику состояний.

    Типичные ошибки

    Ошибка 1: className затирает все существующие классы

    // Элемент имеет: class="btn btn-large"
    element.className = 'active'  // Теперь только 'active' — потеряли btn и btn-large!
    
    // Правильно
    element.classList.add('active')  // Добавляет к существующим классам

    Ошибка 2: забыли единицу в style

    element.style.width = 100       // не работает! нет 'px'
    element.style.width = '100px'   // правильно
    element.style.fontSize = 16     // не работает
    element.style.fontSize = '16px' // правильно

    Ошибка 3: читают style вместо getComputedStyle

    // Если стиль задан через CSS-файл, а не инлайн:
    element.style.color  // '' — пустая строка! не видит CSS-файлы
    
    // Правильно для чтения итогового значения
    getComputedStyle(element).color  // 'rgb(33, 150, 243)'

    В реальных проектах

  • Тёмная тема: переключение CSS-класса dark на body или через CSS-переменные
  • Skeleton-экраны: класс loading пока данные загружаются
  • Тосты/уведомления: добавление/удаление класса visible с CSS-анимацией
  • Drag and Drop: инлайновые стили для позиционирования перетаскиваемого элемента
  • Примеры

    Симуляция classList API через Set и работа с dataset

    // Симуляция element.classList через Set
    function createMockElement(tag, initialClasses) {
      if (initialClasses === undefined) initialClasses = []
      const classSet = new Set(initialClasses)
      const dataset = {}
    
      return {
        tag,
        dataset,
        classList: {
          add(...classes) {
            classes.forEach(c => classSet.add(c))
          },
          remove(...classes) {
            classes.forEach(c => classSet.delete(c))
          },
          toggle(cls, force) {
            if (force !== undefined) {
              force ? classSet.add(cls) : classSet.delete(cls)
            } else {
              classSet.has(cls) ? classSet.delete(cls) : classSet.add(cls)
            }
            return classSet.has(cls)
          },
          contains(cls) { return classSet.has(cls) },
          replace(oldCls, newCls) {
            if (classSet.has(oldCls)) {
              classSet.delete(oldCls)
              classSet.add(newCls)
              return true
            }
            return false
          },
          get value() { return [...classSet].join(' ') }
        }
      }
    }
    
    // Кнопка добавления в корзину
    const btn = createMockElement('button', ['btn', 'btn-primary'])
    console.log(btn.classList.value)              // 'btn btn-primary'
    
    // Добавляем в корзину — меняем вид кнопки
    btn.classList.replace('btn-primary', 'btn-success')
    btn.classList.add('btn--added')
    console.log(btn.classList.value)              // 'btn btn-success btn--added'
    console.log(btn.classList.contains('btn-success'))  // true
    
    // dataset — хранение данных в элементе
    btn.dataset.productId = '42'
    btn.dataset.quantity = '1'
    console.log(btn.dataset.productId)            // '42'
    
    // Skeleton-экран: переключение состояния загрузки
    const card = createMockElement('div', ['card'])
    card.classList.add('loading')
    console.log(card.classList.contains('loading'))  // true
    card.classList.remove('loading')
    card.classList.add('loaded')
    console.log(card.classList.value)             // 'card loaded'
    
    // toggle с force — принудительное состояние (открыть/закрыть меню)
    const menu = createMockElement('nav', ['menu'])
    menu.classList.toggle('open', true)           // открыть
    console.log(menu.classList.contains('open'))  // true
    menu.classList.toggle('open', false)          // закрыть
    console.log(menu.classList.contains('open'))  // false

    Задание

    Напиши функцию createClassList(initial) которая возвращает объект с методами classList API: add(...classes), remove(...classes), toggle(cls, force), contains(cls), entries() — возвращает массив всех классов, и геттер length. Реализация через Set.

    Подсказка

    classes — Set. add: newClasses.forEach(c => classes.add(c)). remove: removeClasses.forEach(c => classes.delete(c)). toggle без force: classes.has(cls) ? classes.delete(cls) : classes.add(cls); return classes.has(cls). entries(): return [...classes].

    Загружаем среду выполнения...
    Загружаем AI-помощника...