diff --git a/projects/natural/src/lib/modules/common/common-module.ts b/projects/natural/src/lib/modules/common/common-module.ts
index 3ec10fe8..8fc7180a 100644
--- a/projects/natural/src/lib/modules/common/common-module.ts
+++ b/projects/natural/src/lib/modules/common/common-module.ts
@@ -12,6 +12,7 @@ import {NaturalDefaultPipe} from './pipes/default.pipe';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
+import {NaturalSrcDensityDirective} from './directives/src-density.directive';
const declarationsToExport = [
NaturalCapitalizePipe,
@@ -21,6 +22,7 @@ const declarationsToExport = [
NaturalSwissDatePipe,
ReactiveAsteriskDirective,
NaturalHttpPrefixDirective,
+ NaturalSrcDensityDirective,
NaturalLinkableTabDirective,
];
diff --git a/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts b/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts
new file mode 100644
index 00000000..a7ecab11
--- /dev/null
+++ b/projects/natural/src/lib/modules/common/directives/src-density.directive.spec.ts
@@ -0,0 +1,69 @@
+import {Component, DebugElement} from '@angular/core';
+import {ComponentFixture, TestBed} from '@angular/core/testing';
+import {NaturalSrcDensityDirective} from './src-density.directive';
+import {By} from '@angular/platform-browser';
+
+@Component({
+ template: `
+
+
+
+
+
+
+ `,
+})
+class TestComponent {}
+
+describe('NaturalSrcDensityDirective', () => {
+ let fixture: ComponentFixture;
+ let elements: DebugElement[]; // the three elements w/ the directive
+
+ const expectedSrc = 'https://example.com/image/123/200';
+ const expectedSrcset =
+ 'https://example.com/image/123/200, https://example.com/image/123/300 1.5x, https://example.com/image/123/400 2x, https://example.com/image/123/600 3x, https://example.com/image/123/800 4x';
+ beforeEach(() => {
+ fixture = TestBed.configureTestingModule({
+ declarations: [NaturalSrcDensityDirective, TestComponent],
+ }).createComponent(TestComponent);
+
+ fixture.detectChanges(); // initial binding
+
+ // all elements with an attached NaturalSrcDensityDirective
+ elements = fixture.debugElement.queryAll(By.directive(NaturalSrcDensityDirective));
+ });
+
+ it('should have 6 active elements', () => {
+ expect(elements.length).toBe(6);
+ });
+
+ it('1st should be kept empty and should not be used in real world', () => {
+ expect(elements[0].nativeElement.src).toBe('');
+ expect(elements[0].nativeElement.srcset).toBe('');
+ });
+
+ it('2nd should work', () => {
+ expect(elements[1].nativeElement.src).toBe(expectedSrc);
+ expect(elements[1].nativeElement.srcset).toBe(expectedSrcset);
+ });
+
+ it('3rd should work', () => {
+ expect(elements[2].nativeElement.src).toBe(expectedSrc);
+ expect(elements[2].nativeElement.srcset).toBe(expectedSrcset);
+ });
+
+ it('4th should keep naturalSrcDensity as-is without srcset', () => {
+ expect(elements[3].nativeElement.src).toBe('https://example.com/image/123.jpg');
+ expect(elements[3].nativeElement.srcset).toBe('');
+ });
+
+ it('5th will completely discard original src and srcset attributes', () => {
+ expect(elements[4].nativeElement.src).toBe(expectedSrc);
+ expect(elements[4].nativeElement.srcset).toBe(expectedSrcset);
+ });
+
+ it('6th will completely discard original src and srcset attributes while keeping naturalSrcDensity as-is', () => {
+ expect(elements[5].nativeElement.src).toBe('https://example.com/image/123.jpg');
+ expect(elements[5].nativeElement.srcset).toBe('');
+ });
+});
diff --git a/projects/natural/src/lib/modules/common/directives/src-density.directive.ts b/projects/natural/src/lib/modules/common/directives/src-density.directive.ts
new file mode 100644
index 00000000..fe8b5f12
--- /dev/null
+++ b/projects/natural/src/lib/modules/common/directives/src-density.directive.ts
@@ -0,0 +1,55 @@
+import {Directive, ElementRef, Input} from '@angular/core';
+
+@Directive({
+ selector: 'img[naturalSrcDensity]',
+})
+export class NaturalSrcDensityDirective {
+ /**
+ * Automatically apply image selection based on screen density.
+ *
+ * The given URL **MUST** be the normal density URL. And it **MUST** include
+ * the size as last path segment. That size will automatically be changed
+ * for other screen densities. That means that the server **MUST** be able to
+ * serve an image of the given size.
+ *
+ * Usage:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * Will generate something like:
+ *
+ * ```html
+ *
+ * ```
+ *
+ * @see https://web.dev/codelab-density-descriptors/
+ */
+ @Input()
+ public set naturalSrcDensity(src: string) {
+ if (!src) {
+ return;
+ }
+
+ const match = src.match(/^(.*\/)(\d+)$/);
+ const base = match?.[1];
+ const size = +(match?.[2] ?? 0);
+
+ let srcset = '';
+ if (base && size) {
+ // This should cover most common densities according to https://www.mydevice.io/#tab1
+ const size15 = size * 1.5;
+ const size2 = size * 2;
+ const size3 = size * 3;
+ const size4 = size * 4;
+
+ srcset = `${base}${size}, ${base}${size15} 1.5x, ${base}${size2} 2x, ${base}${size3} 3x, ${base}${size4} 4x`;
+ }
+
+ this.elementRef.nativeElement.src = src;
+ this.elementRef.nativeElement.srcset = srcset;
+ }
+
+ constructor(private elementRef: ElementRef) {}
+}
diff --git a/projects/natural/src/lib/modules/common/public-api.ts b/projects/natural/src/lib/modules/common/public-api.ts
index 41dc41e3..c80451b7 100644
--- a/projects/natural/src/lib/modules/common/public-api.ts
+++ b/projects/natural/src/lib/modules/common/public-api.ts
@@ -11,6 +11,7 @@ export * from './pipes/ellipsis.pipe';
export * from './pipes/enum.pipe';
export * from './pipes/swiss-date.pipe';
export * from './services/memory-storage';
+export {NaturalSrcDensityDirective} from './directives/src-density.directive';
export {
NATURAL_SEO_CONFIG,
NaturalSeoConfig,