From 5c290f72e92e835a96f5a2189a15f3031dc309b5 Mon Sep 17 00:00:00 2001 From: Damian Glowala Date: Fri, 27 Feb 2026 19:41:46 +0100 Subject: [PATCH 1/3] feat(nuxt-img): allow setting `placeholder` and `placeholderClass` via presets --- src/runtime/components/NuxtImg.vue | 78 ++++++++++++++++++------------ src/types/image.ts | 6 +++ 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/runtime/components/NuxtImg.vue b/src/runtime/components/NuxtImg.vue index 53ecad0eb..1868b9642 100644 --- a/src/runtime/components/NuxtImg.vue +++ b/src/runtime/components/NuxtImg.vue @@ -2,7 +2,7 @@ @@ -45,6 +45,7 @@ const emit = defineEmits<{ defineSlots<{ default(props: DefaultSlotProps): any }>() const $img = useImage() + const { providerOptions, normalizedAttrs, imageModifiers } = useImageProps(props) const sizes = computed(() => $img.getSizes(props.src!, { @@ -56,43 +57,46 @@ const sizes = computed(() => $img.getSizes(props.src!, { const placeholderLoaded = ref(false) -const attrs = useAttrs() as ImgHTMLAttributes -const imgAttrs = computed(() => ({ - ...normalizedAttrs.value, - 'data-nuxt-img': '', - ...(!props.placeholder || placeholderLoaded.value) - ? { sizes: sizes.value.sizes, srcset: sizes.value.srcset } - : {}, - ...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, - ...attrs, -})) +const _placeholder = computed( + () => props.placeholder || (props.preset && $img.options.presets[props.preset]?.placeholder), +) + +const _placeholderClass = computed( + () => props.placeholderClass || (props.preset && $img.options.presets[props.preset]?.placeholderClass), +) -const placeholder = computed(() => { +const placeholderSrc = computed(() => { if (placeholderLoaded.value) { - return false + return undefined } - const placeholder = props.placeholder === '' ? [10, 10] : props.placeholder + const src = _placeholder.value - if (!placeholder) { - return false + if (!src) { + return undefined } - if (typeof placeholder === 'string') { - return placeholder + if (typeof src === 'string') { + return src } - const [width = 10, height = width, quality = 50, blur = 3] = Array.isArray(placeholder) - ? placeholder - : typeof placeholder === 'number' ? [placeholder] : [] - - return $img(props.src!, { - ...imageModifiers.value, - width, - height, - quality, - blur, - }, providerOptions.value) + const [width = 10, height = width, quality = 50, blur = 3] = Array.isArray(src) + ? src + : typeof src === 'number' + ? [src] + : [] + + return $img( + props.src!, + { + ...imageModifiers.value, + width, + height, + quality, + blur, + }, + providerOptions.value, + ) }) const mainSrc = computed(() => @@ -101,7 +105,19 @@ const mainSrc = computed(() => : $img(props.src!, imageModifiers.value, providerOptions.value), ) -const src = computed(() => placeholder.value || mainSrc.value) +const src = computed(() => placeholderSrc.value || mainSrc.value) + +const attrs = useAttrs() as ImgHTMLAttributes + +const imgAttrs = computed(() => ({ + ...normalizedAttrs.value, + 'data-nuxt-img': '', + ...!placeholderSrc.value + ? { sizes: sizes.value.sizes, srcset: sizes.value.srcset } + : {}, + ...import.meta.server ? { onerror: 'this.setAttribute(\'data-error\', 1)' } : {}, + ...attrs, +})) if (import.meta.server && props.preload) { const hasMultipleDensities = sizes.value.srcset.includes('x, ') @@ -134,7 +150,7 @@ const imgEl = useTemplateRef('imgEl') defineExpose({ imgEl }) onMounted(() => { - if (placeholder.value || props.custom) { + if (placeholderSrc.value || props.custom) { const img = new Image() if (mainSrc.value) { diff --git a/src/types/image.ts b/src/types/image.ts index ae0f06ffc..0133aba3e 100644 --- a/src/types/image.ts +++ b/src/types/image.ts @@ -23,6 +23,12 @@ export interface ImageOptions> & ('modifiers' extends keyof ConfiguredImageProviders[Provider] ? ConfiguredImageProviders[Provider]['modifiers'] : Record) sizes?: string | Record From 1a0f634f5003ccc6b00c6d86bcfbc1d27a3a2ceb Mon Sep 17 00:00:00 2001 From: Damian Glowala Date: Fri, 27 Feb 2026 20:00:38 +0100 Subject: [PATCH 2/3] update bundle size --- test/unit/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/bundle.test.ts b/test/unit/bundle.test.ts index eea4df32d..8ba3ada0f 100644 --- a/test/unit/bundle.test.ts +++ b/test/unit/bundle.test.ts @@ -21,7 +21,7 @@ describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)('nuxt image bundle size', image: { provider: 'ipx' }, }) - expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.6k"`) + expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.8k"`) }) }) From c804271cfe6deb04d4bd52f896ee85b11410fea9 Mon Sep 17 00:00:00 2001 From: Damian Glowala Date: Mon, 2 Mar 2026 17:59:00 +0100 Subject: [PATCH 3/3] add test --- test/nuxt/image.test.ts | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/test/nuxt/image.test.ts b/test/nuxt/image.test.ts index 60446a74a..98ead9154 100644 --- a/test/nuxt/image.test.ts +++ b/test/nuxt/image.test.ts @@ -392,7 +392,7 @@ describe('Sizes and densities behavior', () => { }) }) -describe('Preset sizes and densities inheritance', () => { +describe('Preset inheritance', () => { const src = '/image.png' beforeEach(() => { @@ -505,6 +505,34 @@ describe('Preset sizes and densities inheritance', () => { expect(srcset).not.toMatch(/\b\d+w\b/) expect(sizes).toBeFalsy() }) + + it('preset placeholder and placeholderClass are inherited', () => { + setImageContext({ + presets: { + withPlaceholder: { + placeholder: true, + placeholderClass: 'placeholder-class', + }, + }, + }) + + const wrapper = mount(NuxtImg, { + propsData: { + src, + width: 200, + height: 200, + preset: 'withPlaceholder', + }, + }) + + const imgElement = wrapper.find('img').element + const domSrc = imgElement.getAttribute('src') + + expect(domSrc).toMatchInlineSnapshot( + '"/_ipx/q_50&blur_3&s_10x10/image.png"', + ) + expect(imgElement.classList).toContain('placeholder-class') + }) }) describe('Renders image, applies module config', () => {