Галерея для сайта собирается двумя путями. Первый — готовый блок конструктора, плагин CMS или виджет: без кода, настройкой в админке. Второй — своя вёрстка: сетка на CSS Grid, лайтбокс на теге <dialog> и тридцать строк JavaScript. Выбор решает не «сложность», а то, кто будет добавлять фотографии после запуска и насколько вам нужен контроль над разметкой, скоростью и внешним видом.
Ниже — обе ветки. Сначала честное сравнение готовых решений, потом пошаговая инструкция по сборке своей галереи на чистом JavaScript: сетка, лайтбокс, стрелки, клавиатура, ленивая загрузка. Весь код в статье мы прогнали в headless-браузере — числа в тексте это то, что печатает консоль, а не то, что «должно получиться».
- Готовое решение — когда фотографии меняет не разработчик и сайт уже живёт на CMS или конструкторе.
- Свой код — когда галерея часть дизайна, важна скорость и не хочется тянуть чужую библиотеку ради трёх функций.
- Лайтбокс сегодня делается нативным
<dialog>: Esc, затемнение фона и блокировка страницы работают без единой строки JS. - Превью надо делать кнопками, а не
<div>— иначе галерея недоступна с клавиатуры.
Галерея для сайта: готовое решение или свой код

Развилка проходит не по бюджету, а по тому, кто трогает контент. Если фотографии добавляет владелец сайта или контент-менеджер, любой код в вёрстке становится обузой: каждое новое фото придётся вставлять руками. Если галерея — часть продуманного макета и обновляется раз в квартал, готовый плагин наоборот мешает: он тащит свой CSS, свою разметку и свои зависимости.
| Вариант | Что это | Когда подходит | Чем платите |
|---|---|---|---|
| Блок конструктора | Готовый блок в Тильде, Wix, Nicepage и подобных | Сайт уже на конструкторе, фото меняет не разработчик | Ограничения шаблона, привязка к платформе |
| Плагин CMS | Галерея как блок или плагин WordPress | Есть CMS и редакторы контента | Лишние скрипты и стили на всех страницах |
| Виджет-сервис | Встраиваемый код стороннего сервиса | Нужно быстро и на любом движке | Внешний запрос, часто платный тариф |
| JS-библиотека | Готовый скрипт лайтбокса или слайдера | Нужны сложные эффекты и жесты | Вес библиотеки, обновления и правки чужого кода |
| Свой код | HTML + CSS Grid + <dialog> + JavaScript |
Галерея встроена в дизайн, важна скорость | Время разработчика на сборку и поддержку |
Проверить себя можно тремя вопросами. Кто добавит десятое фото — разработчик или менеджер? Сколько снимков будет через год: двадцать или две тысячи? Галерея должна выглядеть как остальной сайт или сойдёт стандартный шаблон? Два ответа «менеджер, много, сойдёт шаблон» — берите готовое: галерея в конструкторе вроде Тильды собирается мышкой, а в WordPress хватит стандартных блоков и их шаблонов. Ответы «разработчик, немного, как остальной сайт» — дальше по статье.
Отдельный случай — когда нужна не сетка, а именно пролистывание по одному кадру. Это уже не галерея, а слайдер, и правила у него другие: там свои требования к жестам, автопрокрутке и доступности. Разбор форматов — в материале про карусели и слайдеры в вёрстке.
Вывод раздела: готовое решение экономит время на старте и забирает его на поддержке чужих обновлений. Свой код требует пары часов сразу, зато дальше не ломается от обновления плагина.

- ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросети DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ
- Где и как применять? Потестируем модель после установки на разных задачах
- Как дообучить модель под себя?
Разметка и сетка: галерея на CSS Grid
Начинаем с разметки. Каждое превью — это <button> с картинкой внутри, а не <div> с обработчиком клика. Причина простая: кнопка попадает в порядок обхода по Tab и срабатывает по Enter и пробелу сама, без атрибута tabindex и без обработчиков клавиатуры. Мы проверили это в headless-браузере: первый Tab по странице встаёт на первую превью, Enter открывает лайтбокс.
У каждой картинки обязательны width и height. Документация MDN объясняет зачем: «Including height and width enables the aspect ratio of the image to be calculated by the browser prior to the image being loaded. This aspect ratio is used to reserve the space needed to display the image, reducing or even preventing a layout shift». Без них страница дёргается, пока грузятся фотографии.
<div class="gallery" id="gallery">
<button class="gallery__item" type="button">
<img src="photo-1-400.jpg"
data-full="photo-1-1600.jpg"
width="400" height="300"
loading="lazy" decoding="async"
alt="Кухня в светлых тонах, вид от окна">
</button>
<button class="gallery__item" type="button">
<img src="photo-2-400.jpg"
data-full="photo-2-1600.jpg"
width="400" height="300"
loading="lazy" decoding="async"
alt="Гостиная с диваном и торшером">
</button>
</div>
Атрибут data-full хранит путь к большой версии снимка: в сетке грузится лёгкое превью, крупный файл подтягивается только при открытии лайтбокса.
Сетку задаёт одна строка CSS. Ключевое слово auto-fill в функции repeat() по описанию MDN «repeats to fill a space. Resolves to the largest number of repetitions that does not cause overflow of a constrained content box» — то есть браузер сам считает, сколько колонок влезет, а minmax(180px, 1fr) не даёт им стать уже 180 пикселей. Медиазапросы для этого не нужны.
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
}
.gallery__item {
padding: 0;
border: 0;
background: none;
cursor: pointer;
border-radius: 8px;
overflow: hidden;
}
.gallery__item img {
display: block;
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
transition: transform .25s ease;
}
.gallery__item:hover img,
.gallery__item:focus-visible img { transform: scale(1.04); }
Сколько колонок получится — можно не гадать, а спросить у браузера. Эта строка печатает число колонок, которое сетка реально построила:
console.log(
getComputedStyle(document.getElementById('gallery'))
.gridTemplateColumns.split(' ').length
);
В нашем прогоне при ширине окна 1200 пикселей и галерее во всю ширину страницы вывод — 6, при 390 пикселях — 2. Никаких медиазапросов в коде нет: одна строка grid-template-columns закрывает и десктоп, и телефон.
Пара aspect-ratio: 4 / 3 и object-fit: cover решает вечную проблему галерей — разнокалиберные исходники. Вертикальные и горизонтальные снимки встают в одинаковые плитки, лишнее обрезается по центру, сетка не рвётся.
Дальше делаем красиво: последние две строки увеличивают превью на 4% под курсором и при переходе по Tab. Эффект висит на :focus-visible, а не только на :hover, — иначе тот, кто ходит по сайту с клавиатуры, не поймёт, где сейчас находится.
Лайтбокс на теге dialog: крупный снимок без библиотек
Раньше ради модального окна подключали библиотеку. Сейчас в HTML есть элемент <dialog>. По документации MDN он «is used to create both modal and non-modal dialog boxes. Modal dialog boxes block interaction with other UI elements, making the rest of the page inert», а страница поддержки помечает элемент как Baseline Widely available: «It’s been available across browsers since March 2022».
Что вы получаете бесплатно, вызвав showModal():
- Esc закрывает окно. Дословно из MDN: «By default, a dialog invoked by the
showModal()method can be dismissed by pressing the Esc key». - Затемнение фона стилизуется псевдоэлементом
::backdrop— отдельныйdiv-подложка не нужен. - Остальная страница становится инертной: фокус не убегает из окна на ссылки под ним.
Разметка окна и его стили. Атрибут autofocus стоит на кнопке закрытия — MDN рекомендует именно так: «it is recommended to add autofocus to the close button inside the dialog». А вот tabindex на самом <dialog> ставить нельзя, документация выносит это отдельным предупреждением.
<dialog class="lightbox" id="lightbox" aria-label="Просмотр фотографии">
<img id="lightbox-img" alt="">
<div class="lightbox__bar">
<button type="button" id="prev" aria-label="Предыдущее фото">←</button>
<span id="lightbox-counter"></span>
<button type="button" id="next" aria-label="Следующее фото">→</button>
<button type="button" id="close" autofocus aria-label="Закрыть просмотр">×</button>
</div>
</dialog>
.lightbox {
border: 0;
padding: 0;
max-width: 92vw;
max-height: 92vh;
background: #111;
color: #fff;
}
.lightbox::backdrop { background: rgb(0 0 0 / .82); }
.lightbox img { display: block; max-width: 92vw; max-height: 78vh; }
.lightbox__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 8px 12px;
}
.lightbox button {
font-size: 20px;
line-height: 1;
padding: 8px 14px;
border: 0;
border-radius: 6px;
background: #2f2f2f;
color: #fff;
cursor: pointer;
}
Стрелки подписаны атрибутом aria-label, потому что видимый текст в них — символы. Скринридер прочитает «Предыдущее фото», а не «стрелка влево».
Логика на JavaScript: переключение, счётчик, клавиатура

Вся логика галереи — один обработчик на контейнере, одна функция показа и три подписки на кнопки. Обработчик вешается не на каждую превью, а на всю сетку: так добавление новых фотографий не требует переподписки.
const gallery = document.getElementById('gallery');
const lightbox = document.getElementById('lightbox');
const bigImage = document.getElementById('lightbox-img');
const counter = document.getElementById('lightbox-counter');
const buttons = [...gallery.querySelectorAll('.gallery__item')];
let current = 0;
function show(index) {
current = (index + buttons.length) % buttons.length;
const thumb = buttons[current].querySelector('img');
bigImage.src = thumb.dataset.full || thumb.src;
bigImage.alt = thumb.alt;
counter.textContent = (current + 1) + ' / ' + buttons.length;
}
gallery.addEventListener('click', (event) => {
const item = event.target.closest('.gallery__item');
if (!item) return;
show(buttons.indexOf(item));
lightbox.showModal();
});
document.getElementById('prev').addEventListener('click', () => show(current - 1));
document.getElementById('next').addEventListener('click', () => show(current + 1));
document.getElementById('close').addEventListener('click', () => lightbox.close());
lightbox.addEventListener('keydown', (event) => {
if (event.key === 'ArrowLeft') show(current - 1);
if (event.key === 'ArrowRight') show(current + 1);
});
Разберём два неочевидных места. Первое — event.target.closest('.gallery__item'). Клик приходит на <img> внутри кнопки, а не на саму кнопку; closest поднимается вверх по дереву и находит нужного предка. Без него обработчик молча не сработает.
Второе — арифметика индекса. Выражение (index + buttons.length) % buttons.length зацикливает галерею в обе стороны: после последнего кадра идёт первый, перед первым — последний. Проверьте на трёх фотографиях:
const total = 3;
const step = (i) => (i + total) % total;
console.log([step(0), step(1), step(2), step(3), step(-1)].join(' '));
Вывод — 0 1 2 0 2. Индекс 3 сворачивается в 0, индекс −1 — в 2. Отдельные проверки «если больше длины» и «если меньше нуля» не нужны.
Что дал прогон готовой галереи в headless-браузере: клик по второй превью открывает окно со счётчиком 2 / 3, ArrowRight переводит на 3 / 3, следующий ArrowRight — на 1 / 3, Esc закрывает окно, свойство lightbox.open становится false. Ни одной строки кода на обработку Esc мы не писали.

- Выполним базовые задачи на российских нейросетях и посмотрим на результаты!
- Файл-инструкцию «Как сделать нейро-фотосессию из своего фото бесплатно, без иностранных карт и прочих сложностей»
- 10 способов заработать на нейросетях: Посмотрим рынок вакансий и поймем, где навыки ИИ дадут наибольший профит
Три ошибки в коде галереи, которые видно сразу
Эти три дефекта кочуют из статьи в статью с 2015 года. Первый — прятать неактивные кадры через transform: scale(0). Трансформация не выводит элемент из потока: место в раскладке остаётся занятым. Замерьте сами — два блока по 100 пикселей в ряд, прячем первый двумя способами и смотрим, где стоит второй:
document.body.insertAdjacentHTML('beforeend', `
<div id="row" style="display:flex">
<div id="a" style="width:100px;height:60px;background:#ddd"></div>
<div id="b" style="width:100px;height:60px;background:#999"></div>
</div>`);
const row = document.getElementById('row');
const a = document.getElementById('a');
const b = document.getElementById('b');
const left = () => Math.round(
b.getBoundingClientRect().left - row.getBoundingClientRect().left
);
a.style.transform = 'scale(0)';
console.log('scale(0):', left());
a.style.transform = '';
a.style.display = 'none';
console.log('display:none:', left());
Вывод в нашем прогоне: scale(0): 100 и display:none: 0. То есть после scale(0) соседний блок по-прежнему сдвинут на 100 пикселей — невидимая картинка продолжает занимать своё место. В галерее из десяти фотографий это девять пустых дыр в раскладке.
Правильно прятать неактивные кадры не нужно вовсе: в сетке видны все превью сразу, а крупный снимок живёт в одном <img> внутри диалога, которому мы просто меняем src.
Вторая ошибка — alt вида «Изображение 1». MDN формулирует назначение атрибута прямо: «The alt attribute holds a textual replacement for the image, which is mandatory and incredibly useful for accessibility — screen readers read the attribute value out to their users so they know what the image means». «Изображение 1» не сообщает ничего ни человеку, ни поисковой системе. Пишите, что на снимке: «Кухня в светлых тонах, вид от окна».
Третья — превью на <div> с обработчиком клика. Такая галерея не открывается с клавиатуры вообще: <div> не получает фокус по Tab и не реагирует на Enter. Кнопка делает и то, и другое сама.
Вывод раздела: галерея ломается не в JavaScript, а в разметке. Кнопки вместо div, осмысленные alt, никаких scale(0) вместо скрытия.
Скорость: ленивая загрузка и лёгкие превью
Галерея — самая тяжёлая часть страницы. Два атрибута снимают большую часть проблемы.
Атрибут loading="lazy" по описанию MDN «defers loading the image until it reaches a calculated distance from the viewport, as defined by the browser. Lazy loading avoids the network and storage bandwidth required to handle the image until it’s reasonably certain that it will be needed». Для сетки из тридцати снимков это означает, что браузер тянет только видимые.
Второй атрибут — decoding="async": он подсказывает браузеру декодировать картинку не в один шаг с остальным содержимым, а после того, как страница уже показана.
Отдельная история — размеры файлов. В сетке колонка редко шире 300 пикселей, поэтому в src должно лежать превью, а не исходник с фотоаппарата. Если превью нужны разные под ретину и мобильный, подключается srcset с sizes:
<img src="photo-1-400.jpg"
srcset="photo-1-400.jpg 400w, photo-1-800.jpg 800w"
sizes="(max-width: 600px) 45vw, 220px"
width="400" height="300"
loading="lazy" decoding="async"
alt="Кухня в светлых тонах, вид от окна">
Крупный файл из data-full загрузится один раз при открытии лайтбокса — и только тот, который посетитель действительно открыл.
Мобильные: свайп или горизонтальная лента
На телефоне сетка из двух колонок работает нормально, а вот лайтбокс просит листания пальцем. Есть два пути.
Первый — оставить кнопки «вперёд-назад», но сделать их крупными: увеличить padding, чтобы в них попадали пальцем, а не курсором. Это честный минимум: работает везде, ничего не ломает.
Второй — сделать содержимое лайтбокса горизонтальной лентой на CSS: overflow-x: auto плюс scroll-snap-type: x mandatory на контейнере и scroll-snap-align: center на кадрах. Тогда пролистывание — родная прокрутка браузера, с инерцией и прилипанием к кадру, без единой строки обработки касаний:
.lightbox__track {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.lightbox__track img {
flex: 0 0 100%;
scroll-snap-align: center;
}
Минус второго пути — в ленту придётся положить все крупные снимки сразу, поэтому он хорош для галерей на 10–15 кадров, а не на две сотни.
Чек-лист: что проверить перед публикацией галереи
| Проверка | Как убедиться |
|---|---|
Превью — это <button> |
Tab доходит до превью, Enter открывает лайтбокс |
У всех картинок есть width и height |
Страница не дёргается при загрузке |
alt описывает содержимое |
По тексту alt понятно, что на фото, без просмотра |
Стоит loading="lazy" |
В панели «Сеть» грузятся только видимые превью |
| В сетке лежат превью, а не исходники | Вес одного файла — сотни килобайт, не мегабайты |
| Лайтбокс закрывается по Esc | lightbox.open становится false |
| Стрелки работают с клавиатуры | ArrowLeft и ArrowRight меняют счётчик |
| Индекс зацикливается | После последнего кадра идёт первый |
| Кнопки на мобильном крупные | В кнопку попадаешь пальцем с первого раза |
Собранная по этому списку галерея для сайта работает без единой внешней зависимости и не ломается при обновлении CMS или плагина. Если фотографии будет добавлять не разработчик, вернитесь к первому разделу — там развилка решается в три вопроса.
Частые вопросы про галерею для сайта
Как сделать галерею для сайта без программирования?
Через блок конструктора или плагин CMS. В Тильде и подобных платформах галерея добавляется готовым блоком, в WordPress — стандартным блоком «Галерея», на любом движке — встраиваемым кодом виджет-сервиса. Кода писать не нужно, но вы принимаете ограничения шаблона и лишние скрипты платформы.
Нужна ли библиотека вроде lightGallery или Fancybox для лайтбокса?
Нет, если нужен обычный просмотр крупного снимка со стрелками. Модальное окно, затемнение фона, закрытие по Esc и блокировку страницы даёт нативный элемент dialog. Библиотека оправдана, когда нужны видео, зум жестами, миниатюры-полоса и подгрузка альбомов.
Сколько кода нужно на свою галерею?
Рабочая версия из этой статьи — 30 строк JavaScript и около полусотни строк CSS на сетку и лайтбокс вместе. Всё остальное делают браузер и разметка: сетку считает grid-template-columns, клавиатуру берут на себя button и dialog.
Как сделать галерею адаптивной без медиазапросов?
Строкой grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)). Браузер сам считает, сколько колонок шириной не меньше 180 пикселей помещается в контейнер. В нашем прогоне при окне 1200 пикселей получилось 6 колонок, при 390 пикселях — 2, и ни одного медиазапроса в коде.
Почему картинки в галерее грузятся долго?
Чаще всего потому, что в сетку положены исходники вместо превью, а атрибут loading=»lazy» не проставлен. Превью под колонку 300 пикселей должно весить сотни килобайт, а не мегабайты, крупная версия подгружается только при открытии лайтбокса.
Документация по использованным элементам: описание тега <dialog> на MDN. Если хочется собрать галерею и весь сайт без ручной вёрстки, посмотрите обзор конструктора Framer с нейросетью.
- Выполним базовые задачи на российских нейросетях и посмотрим на результаты!
- Файл-инструкцию «Как сделать нейро-фотосессию из своего фото бесплатно, без иностранных карт и прочих сложностей»
- Покажем 10+ способов улучшить свою жизнь с ИИ каждому — от ребенка и пенсионера до управленца и предпринимателя
- Возможность получить Доступ в Нейроклуб на целый месяц
- Как ИИ ускоряет работу и приносит деньги
- За 2 часа вы получите четкий план, как начать работать с ИИ прямо сейчас!
- Освой нейросеть Perplexity и узнай, как пользоваться функционалом остальных ИИ в одном
- УЧАСТВОВАТЬ ЗА 0 РУБ.
- Расскажем, как получить подписку
- ПОКАЖЕМ, КАК РАЗВЕРНУТЬ МОДЕЛЬ нейросеть DEEPSEEK R1 ПРЯМО НА СВОЁМ КОМПЬЮТЕРЕ