Skip to content

Адаптивные изображения ​

srcset + sizes ​

Передайте widths, чтобы автоматически сгенерировать атрибут srcset:

vue
<VImage
  src="/photo.jpg"
  alt="Photo"
  :widths="[400, 800, 1200]"
  sizes="(max-width: 768px) 100vw, 50vw"
/>

Рендерит:

html
<img
  src="/photo.jpg"
  srcset="/photo.jpg 400w, /photo.jpg 800w, /photo.jpg 1200w"
  sizes="(max-width: 768px) 100vw, 50vw"
  alt="Photo"
/>

Если widths не задан, srcset не добавляется — используется обычный src. Если widths задан, а sizes — нет, sizes по умолчанию становится "100vw".

Дескрипторы плотности (1x / 2x / 3x) ​

Для изображений фиксированного размера — иконок, аватаров, логотипов — используйте densities вместо widths. Браузер выбирает кандидата, соответствующего плотности пикселей устройства; sizes не нужен. densities имеет приоритет над widths (два типа дескрипторов нельзя смешивать в одном srcset).

:densities принимает две формы:

vue
<!-- 1. Карта URL по плотности — отдельные файлы (рекомендуется для статичных ассетов). -->
<VImage
  src="/avatar.png"
  alt="Avatar"
  :width="48"
  :height="48"
  :densities="{ 1: '/avatar.png', 2: '/avatar@2x.png', 3: '/avatar@3x.png' }"
/>
<!-- → srcset="/avatar.png 1x, /avatar@2x.png 2x, /avatar@3x.png 3x" -->

<!-- 2. Список плотностей — переиспользует единственный `src` для каждой плотности. Полезно
     только когда сам URL учитывает разрешение (эндпоинт CDN/DPR). -->
<VImage src="https://cdn.example.com/avatar?dpr=auto" alt="Avatar" :densities="[1, 2, 3]" />
<!-- → srcset="…?dpr=auto 1x, …?dpr=auto 2x, …?dpr=auto 3x" -->

Использование утилит напрямую:

ts
import { generateSrcset, generateSizes, generateDensitySrcset } from '@macrulez/vue-image-kit'

generateSrcset('/photo.jpg', [400, 800, 1200])
// → '/photo.jpg 400w, /photo.jpg 800w, /photo.jpg 1200w'

generateSizes('(max-width: 768px) 100vw, 50vw')
// → '(max-width: 768px) 100vw, 50vw'

generateSizes()
// → '100vw'

generateDensitySrcset('/logo.png', [1, 2, 3])
// → '/logo.png 1x, /logo.png 2x, /logo.png 3x'

// Отдельные файлы на плотность через карту URL:
generateDensitySrcset({ 1: '/a.png', 2: '/a@2x.png' }, [1, 2])
// → '/a.png 1x, /a@2x.png 2x'

Переключение источников WebP / AVIF ​

Когда src — объект вместо строки, <VImage> рендерит элемент <picture> с соответствующими элементами <source>:

vue
<VImage
  :src="{
    avif: '/photo.avif',
    webp: '/photo.webp',
    fallback: '/photo.jpg',
  }"
  alt="Photo"
  :width="1200"
  :height="800"
/>

Рендерит:

html
<picture>
  <source srcset="/photo.avif" type="image/avif" />
  <source srcset="/photo.webp" type="image/webp" />
  <img src="/photo.jpg" alt="Photo" width="1200" height="800" />
</picture>

Браузер выбирает первый поддерживаемый формат. Если задан только webp, добавляется только один <source>. fallback требуется всегда.

Объект SrcSet ​

ts
interface SrcSet {
  avif?: string // URL версии AVIF
  webp?: string // URL версии WebP
  fallback: string // Обязателен — оригинальный формат (JPEG/PNG)
}

Адаптивные источники (арт-дирекция) ​

Используйте, когда нужно отдавать принципиально другое изображение (другая обрезка, другая композиция) в зависимости от размера экрана. Реализовано через именованные брейкпоинты — браузер выбирает первый подходящий <source media="...">.

Глобальные брейкпоинты (задаются один раз при установке плагина) ​

ts
// main.ts
app.use(VImageKitPlugin, {
  breakpoints: {
    sm: '(max-width: 640px)',
    md: '(max-width: 1024px)',
    lg: '(min-width: 1025px)',
  },
})

Использование в компонентах — только ключи ​

vue
<VImage
  src="/hero-desktop.jpg"
  alt="Hero"
  :sources="{
    sm: '/hero-mobile.jpg',
    md: '/hero-tablet.jpg',
  }"
/>

Генерирует:

html
<picture>
  <source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
  <source media="(max-width: 1024px)" srcset="/hero-tablet.jpg" />
  <img src="/hero-desktop.jpg" alt="Hero" />
</picture>

Порядок <source> устанавливается автоматически по возрастанию max-width — это требование <picture>, который выбирает первый подходящий source.

Брейкпоинты на уровне компонента ​

Сливаются с глобальными брейкпоинтами. Локальные ключи имеют приоритет при конфликте:

vue
<VImage
  src="/product-desktop.jpg"
  alt="Product"
  :breakpoints="{
    xs: '(max-width: 375px)',
    wide: '(min-width: 1600px)',
  }"
  :sources="{
    xs: '/product-xs.jpg',
    sm: '/product-mobile.jpg',
    md: '/product-tablet.jpg',
    wide: '/product-wide.jpg',
  }"
/>

Итоговый <picture> содержит элементы <source> для xs, sm, md (из слитых брейкпоинтов) и wide — отсортированные автоматически.

Комбинирование с AVIF/WebP ​

Адаптивные источники (sources) и источники по формату (src как объект) независимы и рендерятся вместе:

vue
<VImage
  :src="{ avif: '/hero.avif', webp: '/hero.webp', fallback: '/hero.jpg' }"
  :sources="{ sm: '/hero-mobile.jpg' }"
  alt="Hero"
/>
html
<picture>
  <source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
  <source srcset="/hero.avif" type="image/avif" />
  <source srcset="/hero.webp" type="image/webp" />
  <img src="/hero.jpg" alt="Hero" />
</picture>

Это покрывает вариант «один набор обрезки + один набор форматов, независимо друг от друга». Для другой обрезки и других форматов на разных брейкпоинтах — например, портретная обрезка AVIF/WebP на мобильном, ландшафтная AVIF/WebP на десктопе — значение брейкпоинта в sources само может быть объектом { avif?, webp?, fallback } вместо обычного URL:

vue
<VImage
  alt="Hero"
  :sources="{
    sm: { avif: '/hero-mobile.avif', webp: '/hero-mobile.webp', fallback: '/hero-mobile.jpg' },
    md: { webp: '/hero-tablet.webp', fallback: '/hero-tablet.jpg' },
  }"
  src="/hero-desktop.jpg"
/>
html
<picture>
  <source media="(max-width: 640px)" srcset="/hero-mobile.avif" type="image/avif" />
  <source media="(max-width: 640px)" srcset="/hero-mobile.webp" type="image/webp" />
  <source media="(max-width: 640px)" srcset="/hero-mobile.jpg" />
  <source media="(max-width: 1024px)" srcset="/hero-tablet.webp" type="image/webp" />
  <source media="(max-width: 1024px)" srcset="/hero-tablet.jpg" />
  <img src="/hero-desktop.jpg" alt="Hero" />
</picture>

Брейкпоинты с обычным URL и с объектом формата можно свободно смешивать в одном объекте sources. avif/webp опциональны для каждого брейкпоинта — испускаются только те форматы, которые у вас реально есть.

Размеры на брейкпоинт — без скачка вёрстки при переключении ​

Другая обрезка на другом брейкпоинте обычно означает и другое соотношение сторон — высокая портретная обрезка на десктопе, широкая ландшафтная на планшете. Если не сообщить об этом <VImage>, компонент может резервировать место только по корневым width/height, поэтому плейсхолдер (и превью blurhash, если оно используется) показывают неверные пропорции на любом брейкпоинте, чья обрезка не совпадает с корневым изображением — а сам блок заметно «прыгает», как только загружается настоящее фото.

Чтобы это исправить, задайте записи sources собственные width/height:

vue
<VImage
  src="/desktop-portrait.jpg"
  :width="720"
  :height="1237"
  :sources="{
    tablet: { src: '/tablet-landscape.jpg', width: 1400, height: 700 },
  }"
  :breakpoints="{ tablet: '(max-width: 1024px)' }"
  alt="Hero"
/>

С width/height на записи tablet компонент <VImage> отслеживает, какой брейкпоинт активен прямо сейчас (реактивно — обновляется при изменении размера окна относительно брейкпоинта), и резервирует блок плейсхолдера и декод blurhash под соотношение сторон именно этого брейкпоинта, а не всегда откатывается к корневому изображению. Соответствующий <source> тоже рендерится с настоящими атрибутами width/height, поэтому как только браузер выбирает его, загруженное изображение сохраняет то же зарезервированное место — никакого скачка между плейсхолдером и финальным фото ни на одном брейкпоинте.

width и height в одной записи работают по принципу «оба или ни одного»: если задать только один из них, он отбрасывается (с предупреждением в консоли в режиме разработки) — риск исказить блок не оправдан.

Запись sources также принимает объект ImageMeta напрямую — ту же форму, что выдаёт манифест CLI/Vite-плагина или импорт ?vik на этапе сборки — так что метаданные конкретного брейкпоинта, сгенерированные при сборке, можно передать как есть, не разбирая их вручную:

vue
<script setup>
import tabletMeta from '/tablet-landscape.jpg?vik'
</script>

<template>
  <VImage
    src="/desktop-portrait.jpg"
    :width="720"
    :height="1237"
    :sources="{ tablet: tabletMeta }"
    :breakpoints="{ tablet: '(max-width: 1024px)' }"
    alt="Hero"
  />
</template>

Записи без width/height продолжают работать в точности как раньше — это чисто дополнительная возможность.

Плейсхолдеры на брейкпоинт ​

С плейсхолдером та же проблема, что и с боксом. Blurhash высокого корневого фото, растянутый в широкий бокс планшета, расставляет цвета не там, где они на самом деле. Запись в sources может нести собственный плейсхолдер рядом с src — hazehash, blurhash, thumbhash, placeholder (LQIP data URL) или placeholderColor:

vue
<VImage
  src="/desktop-portrait.jpg"
  :width="720"
  :height="1237"
  blurhash="LEHV6nWB2yk8pyo0adR*.7kCMdnj"
  :sources="{
    tablet: {
      src: '/tablet-landscape.jpg',
      width: 1400,
      height: 700,
      blurhash: 'LKO2?U%2Tw=w]~RBVZRi};RPxuwH',
    },
  }"
  :breakpoints="{ tablet: '(max-width: 1024px)' }"
  alt="Hero"
/>

Пока медиазапрос записи совпадает, <VImage> показывает плейсхолдер этой записи в её пропорциях, а не плейсхолдер корневого изображения.

  • Совпавшая запись заменяет весь набор плейсхолдеров корня целиком, а не объединяется с ним по полям. Корневой placeholderColor не перебивает blurhash источника, а корневой blurhash не показывается за источником, у которого есть только placeholderColor.
  • Запись без собственного плейсхолдера и брейкпоинт, на котором ни одна запись не совпала, используют плейсхолдер корня. placeholderMode действует на всех брейкпоинтах.
  • На сервере медиазапросы не вычисляются, поэтому серверная разметка всегда относится к корневому изображению.
  • Запись-ImageMeta (импорт ?vik, как в примере выше) приносит свой плейсхолдер с собой.
  • Запись в виде простого объекта { avif, webp, fallback } плейсхолдер нести не может; оберните её: { src: { avif, webp, fallback }, blurhash }.

Если зарегистрирован манифест плейсхолдеров, запись манифеста для src источника подставляет те же поля — hazehash, blurhash, thumbhash, color, width и height — везде, где сам источник их не задаёт. Оба варианта умеет генерировать npx vue-image-kit placeholders: см. Источники art direction.

BreakpointMap ​

ts
type BreakpointMap = Record<string, string>
// ключ — произвольное имя, значение — CSS media query

Приоритет брейкпоинтов ​

  • Локальный проп breakpoints на компоненте — высокий приоритет, переопределяет глобальные ключи при конфликте.
  • Глобальные breakpoints из VImageKitPlugin — базовый уровень, доступны во всех компонентах.

Это слияние, как и сортировка sources в упорядоченные записи <source>, выполняется через useBreakpoints() — доступен отдельно для headless-сценариев арт-дирекции.

Утилита для атрибута sizes ​

buildSizes() строит строку атрибута sizes из объекта, ключи которого — брейкпоинты — работает с именованными брейкпоинтами плагина.

ts
import { buildSizes } from '@macrulez/vue-image-kit'

const breakpoints = { sm: '(max-width: 640px)', md: '(max-width: 1024px)' }

buildSizes({ sm: '100vw', md: '50vw', default: '33vw' }, breakpoints)
// → '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw'

Ссылки preload ​

generatePreloadLink() генерирует HTML-строку <link rel="preload"> для критичных изображений над первым экраном. Используйте в Nuxt useHead или внедряйте в SSR <head>, чтобы улучшить LCP.

ts
import { generatePreloadLink, generateSrcset } from '@macrulez/vue-image-kit'

const srcset = generateSrcset('/hero.jpg', [400, 800, 1200])

const link = generatePreloadLink('/hero.jpg', {
  srcset,
  sizes: '100vw',
})
// → '<link rel="preload" as="image" href="/hero.jpg" imagesrcset="..." imagesizes="100vw">'

В Nuxt:

vue
<script setup lang="ts">
import { generatePreloadLink } from '@macrulez/vue-image-kit'

useHead({
  link: [{ innerHTML: generatePreloadLink('/hero.jpg', { sizes: '100vw' }) }],
})
</script>

Компоновка без обёртки ​

<VImage> не рендерится внутри обёртывающего элемента. В любой момент времени это ровно один блок: плейсхолдер до начала загрузки, реальный <img> после — второе полностью заменяет первое, а не оборачивает его. Когда картинке нужен <picture> (объект в src, sources), <picture> служит лишь контейнером для элементов <source>: он рендерится с display: contents, то есть не создаёт собственного блока, и <img> внутри него раскладывается так, будто стоит сам по себе. Все стили из layout (ниже) применяются напрямую к этому единственному блоку.

Практическое следствие: class, style, data-*, обработчики событий и любые другие атрибуты, переданные в <VImage>, всегда попадают на элемент, который показывает картинку, — плейсхолдер, блок ошибки или <img> (в том числе внутри <picture>), — и никогда на промежуточную обёртку. Правило вроде .card-image { width: 100%; height: auto; border-radius: 10px } стилизует саму картинку, с sources или без, и не требует селектора, залезающего внутрь компонента. То же верно для <img>, отрендеренного на сервере, а класс копируется и на изображение внутри запасного <noscript> у картинки с ssrPlaceholder.

Пресеты раскладки ​

Проп layout переключает способ размещения — применяется напрямую к единственному рендерящемуся элементу (см. выше). Если оставить его незаданным, поведение полностью идентично 'responsive' ниже — включая автогенерацию sizes — поэтому нет смысла явно указывать layout="responsive" только ради этой эвристики: незаданный layout уже даёт её.

fixed — точная рамка width×height, без адаптивного масштабирования (как обычный <img width height>):

vue
<VImage src="/icon.jpg" alt="Icon" :width="64" :height="64" layout="fixed" />

responsive — заполняет ширину контейнера, aspect-ratio сохраняется из width/height, и автогенерируется sizes из width, если sizes не задан явно ((min-width: {width}px) {width}px, 100vw — «шириной в свой собственный размер, иначе на всю ширину вьюпорта»). Это и есть в точности то, что вы получаете, оставив layout незаданным:

vue
<VImage
  src="/photo.jpg"
  alt="Photo"
  :width="800"
  :height="600"
  :widths="[400, 800, 1200]"
  layout="responsive"
/>

Без width/height сохранять пропорции не из чего, поэтому <VImage> вообще не навязывает размеры — элемент рендерится в своём естественном размере, полностью под управлением того CSS, что вы зададите сами (например, max-height на своём классе).

fill — абсолютно заполняет позиционированного родителя (position: absolute; inset: 0 прямо на самом элементе); родитель должен иметь position: relative (или аналог). width/height становятся опциональными — типично для hero-баннеров или карточек, где размер задаёт контейнер:

vue
<div style="position: relative; aspect-ratio: 16 / 9;">
  <VImage src="/hero.jpg" alt="Hero" layout="fill" fit="cover" priority />
</div>