🎞 Component
Media Grid
Editorial 2-column layout with one large primary asset and stacked secondary items at a 21:9 aspect ratio.
Interactive Demo Playground
Configure Properties
Usage Code Generator
import MediaGrid from '@contentveda/ui/react/MediaGrid';
import '@contentveda/ui/theme.css';
export default function Example() {
return (
<MediaGrid
primaryMedia={{
id: 'p1',
media: { type: 'image', url: '/primary.jpg' },
mapLinks: [{ url: '/feature' }],
altText: 'Feature story',
}}
secondaryMedia={[
{ id: 's1', media: { type: 'image', url: '/sec1.jpg' }, mapLinks: [{ url: '/story-1' }] },
{ id: 's2', media: { type: 'image', url: '/sec2.jpg' }, mapLinks: [{ url: '/story-2' }] },
]}
/>
);
}
<script setup>
import MediaGrid from '@contentveda/ui/vue/MediaGrid.vue';
import '@contentveda/ui/theme.css';
const primaryMedia = {
id: 'p1',
media: { type: 'image', url: '/primary.jpg' },
mapLinks: [{ url: '/feature' }],
altText: 'Feature story',
};
const secondaryMedia = [
{ id: 's1', media: { type: 'image', url: '/sec1.jpg' }, mapLinks: [{ url: '/story-1' }] },
{ id: 's2', media: { type: 'image', url: '/sec2.jpg' }, mapLinks: [{ url: '/story-2' }] },
];
</script>
<template>
<MediaGrid :primaryMedia="primaryMedia" :secondaryMedia="secondaryMedia" />
</template>
<script lang="ts">
import MediaGrid from '@contentveda/ui/svelte/MediaGrid.svelte';
</script>
<MediaGrid
primaryMedia={{ id: 'p1', media: { type: 'image', url: '/primary.jpg' } }}
secondaryMedia={[{ id: 's1', media: { type: 'image', url: '/sec1.jpg' } }]}
/>
import MediaGrid from '@contentveda/ui/solid/MediaGrid';
import '@contentveda/ui/theme.css';
export default function Example() {
return (
<MediaGrid
primaryMedia={{
id: 'p1',
media: { type: 'image', url: '/primary.jpg' },
mapLinks: [{ url: '/feature' }],
altText: 'Feature story',
}}
secondaryMedia={[
{ id: 's1', media: { type: 'image', url: '/sec1.jpg' }, mapLinks: [{ url: '/story-1' }] },
{ id: 's2', media: { type: 'image', url: '/sec2.jpg' }, mapLinks: [{ url: '/story-2' }] },
]}
/>
);
}
import { Component } from '@angular/core';
import { MediaGridModule } from '@contentveda/ui/angular/MediaGrid';
@Component({
selector: 'app-example',
standalone: true,
imports: [MediaGridModule],
template: `
<media-grid
[primaryMedia]="primaryMedia"
[secondaryMedia]="secondaryMedia"
></media-grid>
`
})
export class ExampleComponent {
primaryMedia = {
id: 'p1',
media: { type: 'image', url: '/primary.jpg' },
mapLinks: [{ url: '/feature' }],
altText: 'Feature story',
};
secondaryMedia = [
{ id: 's1', media: { type: 'image', url: '/sec1.jpg' }, mapLinks: [{ url: '/story-1' }] },
{ id: 's2', media: { type: 'image', url: '/sec2.jpg' }, mapLinks: [{ url: '/story-2' }] },
];
}
<script type="module"
src="node_modules/@contentveda/ui/dist/webcomponent/dist/MediaGrid.js"></script>
<cv-media-grid
primary-media='{"id":"p1","media":{"type":"image","url":"/primary.jpg"}}'
secondary-media='[{"id":"s1","media":{"type":"image","url":"/sec1.jpg"}}]'
></cv-media-grid>
Props API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| primaryMedia required | MediaGridItem | — | The large featured asset occupying 2/3 of the grid width |
| secondaryMedia | MediaGridItem[] | undefined | Optional stacked secondary assets in the right column |
| className | string | undefined | Additional CSS class names on the root element |
| isLoading | boolean | undefined | Applies shimmer skeleton loading animation to the grid |
| lazyLoad | boolean | undefined | Defer mounting media until the grid scrolls near the viewport (default: true) |
| lazyThreshold | number | undefined | IntersectionObserver threshold, 0-1 (default: 0.1) |
| lazyRootMargin | string | undefined | IntersectionObserver rootMargin (default: '200px') |
The secondary column uses
grid-template-rows: 1fr 1fr — all secondary items share equal height regardless of count.