
Security News
PyPI Expands Trusted Publishing to GitLab Self-Managed as Adoption Passes 25 Percent
PyPI adds Trusted Publishing support for GitLab Self-Managed as adoption reaches 25% of uploads
mosaic-grid-widget
Advanced tools
A framework-agnostic web component for creating beautiful, animated mosaic-style content grids with lazy loading and custom previews
A framework-agnostic web component for creating beautiful, animated mosaic-style content grids. Display images, PDFs, videos, markdown files, and external links in a responsive grid layout with smooth expand/collapse animations.
Inspiration: This package was originally inspired by the beautiful mosaic grid design from CodePen by iamsaief. We've reimagined it as a reusable, type-safe web component with enhanced features including lazy loading, custom previews, and support for multiple content types.
requestAnimationFrame and GPU acceleration for smooth interactionsnpm install mosaic-grid-widget
Try the interactive demos with beautiful nature images and various content types:
Run locally:
npm install
npm run dev
Then open one of these URLs in your browser:
http://localhost:5173 - Full-featured demo with 60+ imageshttp://localhost:5173/custom-card.html - Demo showcasing dropdown menus/)The main demo showcases:
/custom-card.html)This demo demonstrates the card overlay system with interactive dropdown menus featuring glassmorphism styling:
⋮ icon in the upper-right corner with a glass-like appearanceThis demo shows how to:
cardOverlays.topRight<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Mosaic Grid Demo</title>
</head>
<body>
<mosaic-grid-widget></mosaic-grid-widget>
<script type="module">
import 'mosaic-grid-widget';
import { MosaicItem } from 'mosaic-grid-widget/types';
const items: MosaicItem[] = [
{
id: '1',
type: 'image',
preview: 'https://example.com/thumb.jpg',
full: 'https://example.com/full.jpg',
layout: 'normal',
title: 'My Image'
},
{
id: '2',
type: 'pdf',
preview: 'https://example.com/pdf-thumb.png',
src: 'https://example.com/document.pdf',
layout: 'tall',
title: 'My PDF'
}
];
document.addEventListener('DOMContentLoaded', () => {
const grid = document.querySelector('mosaic-grid-widget');
if (grid) {
grid.items = items;
}
});
</script>
</body>
</html>
Since mosaic-grid-widget is a web component, it works seamlessly with React. Here's how to use it:
import React, { useEffect, useRef } from 'react';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
function MosaicGrid() {
const gridRef = useRef<HTMLElement>(null);
const [items, setItems] = React.useState<MosaicItem[]>([
{
id: '1',
type: 'image',
preview: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
full: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
layout: 'big',
title: 'Mountain Landscape'
},
{
id: '2',
type: 'image',
preview: 'https://images.unsplash.com/photo-1518837695005-2083093ee35b',
full: 'https://images.unsplash.com/photo-1518837695005-2083093ee35b',
layout: 'wide',
title: 'Ocean View'
}
]);
useEffect(() => {
const grid = gridRef.current;
if (grid) {
// Type assertion needed because React doesn't know about custom element properties
(grid as any).items = items;
}
}, [items]);
return (
<mosaic-grid-widget ref={gridRef} />
);
}
export default MosaicGrid;
For better type safety, you can extend the custom element interface:
import React, { useEffect, useRef } from 'react';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
// Extend the custom element interface for TypeScript
declare global {
namespace JSX {
interface IntrinsicElements {
'mosaic-grid-widget': React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement> & {
items?: MosaicItem[];
},
HTMLElement
>;
}
}
}
interface MosaicGridProps {
items: MosaicItem[];
onItemClick?: (item: MosaicItem) => void;
}
function MosaicGrid({ items, onItemClick }: MosaicGridProps) {
const gridRef = useRef<HTMLElement>(null);
useEffect(() => {
const grid = gridRef.current;
if (grid) {
(grid as any).items = items;
// Listen to custom events if needed
const handleExpand = (e: CustomEvent) => {
if (onItemClick) {
onItemClick(e.detail.item);
}
};
grid.addEventListener('card-expanded', handleExpand as EventListener);
return () => {
grid.removeEventListener('card-expanded', handleExpand as EventListener);
};
}
}, [items, onItemClick]);
return <mosaic-grid-widget ref={gridRef} />;
}
export default MosaicGrid;
Create a reusable hook for easier integration:
import { useEffect, useRef, useState } from 'react';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
function useMosaicGrid(items: MosaicItem[]) {
const gridRef = useRef<HTMLElement>(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const grid = gridRef.current;
if (grid) {
(grid as any).items = items;
setIsReady(true);
}
}, [items]);
const updateItems = (newItems: MosaicItem[]) => {
const grid = gridRef.current;
if (grid) {
(grid as any).items = newItems;
}
};
return { gridRef, isReady, updateItems };
}
// Usage
function MyComponent() {
const [items, setItems] = useState<MosaicItem[]>([...]);
const { gridRef, isReady } = useMosaicGrid(items);
return (
<div>
{isReady && <p>Grid is ready!</p>}
<mosaic-grid-widget ref={gridRef} />
</div>
);
}
import React, { useEffect, useRef, useState } from 'react';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
function MosaicGridWithData() {
const gridRef = useRef<HTMLElement>(null);
const [items, setItems] = useState<MosaicItem[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Fetch data from API
fetch('/api/images')
.then(res => res.json())
.then(data => {
const mosaicItems: MosaicItem[] = data.map((img: any, index: number) => ({
id: img.id,
type: 'image' as const,
preview: img.thumbnail,
full: img.url,
layout: index % 4 === 0 ? 'big' : index % 3 === 0 ? 'wide' : 'normal',
title: img.title
}));
setItems(mosaicItems);
setLoading(false);
});
}, []);
useEffect(() => {
const grid = gridRef.current;
if (grid && items.length > 0) {
(grid as any).items = items;
}
}, [items]);
if (loading) return <div>Loading...</div>;
return <mosaic-grid-widget ref={gridRef} />;
}
Vue 3 works great with web components. Here's how to use mosaic-grid-widget:
<template>
<mosaic-grid-widget ref="gridRef" />
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
const gridRef = ref<HTMLElement | null>(null);
const items = ref<MosaicItem[]>([
{
id: '1',
type: 'image',
preview: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
full: 'https://images.unsplash.com/photo-1506905925346-21bda4d32df4',
layout: 'big',
title: 'Mountain Landscape'
},
{
id: '2',
type: 'image',
preview: 'https://images.unsplash.com/photo-1518837695005-2083093ee35b',
full: 'https://images.unsplash.com/photo-1518837695005-2083093ee35b',
layout: 'wide',
title: 'Ocean View'
}
]);
onMounted(() => {
if (gridRef.value) {
(gridRef.value as any).items = items.value;
}
});
watch(items, (newItems) => {
if (gridRef.value) {
(gridRef.value as any).items = newItems;
}
}, { deep: true });
</script>
<template>
<mosaic-grid-widget ref="grid" />
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
export default defineComponent({
name: 'MosaicGrid',
data() {
return {
items: [
{
id: '1',
type: 'image',
preview: 'https://example.com/thumb.jpg',
full: 'https://example.com/full.jpg',
layout: 'normal',
title: 'My Image'
}
] as MosaicItem[]
};
},
mounted() {
const grid = this.$refs.grid as HTMLElement;
if (grid) {
(grid as any).items = this.items;
}
},
watch: {
items: {
handler(newItems: MosaicItem[]) {
const grid = this.$refs.grid as HTMLElement;
if (grid) {
(grid as any).items = newItems;
}
},
deep: true
}
}
});
</script>
<template>
<mosaic-grid-widget
ref="gridRef"
@card-expanded="handleCardExpanded"
/>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
interface Props {
items: MosaicItem[];
}
const props = defineProps<Props>();
const emit = defineEmits<{
itemClick: [item: MosaicItem];
}>();
const gridRef = ref<HTMLElement | null>(null);
const handleCardExpanded = (event: CustomEvent) => {
emit('itemClick', event.detail.item);
};
onMounted(() => {
if (gridRef.value) {
(gridRef.value as any).items = props.items;
}
});
watch(() => props.items, (newItems) => {
if (gridRef.value) {
(gridRef.value as any).items = newItems;
}
}, { deep: true });
</script>
<template>
<div>
<div v-if="loading">Loading grid...</div>
<mosaic-grid-widget v-else ref="gridRef" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import 'mosaic-grid-widget';
import type { MosaicItem } from 'mosaic-grid-widget/types';
const gridRef = ref<HTMLElement | null>(null);
const items = ref<MosaicItem[]>([]);
const loading = ref(true);
onMounted(async () => {
try {
const response = await fetch('/api/images');
const data = await response.json();
items.value = data.map((img: any, index: number) => ({
id: img.id,
type: 'image' as const,
preview: img.thumbnail,
full: img.url,
layout: index % 4 === 0 ? 'big' : index % 3 === 0 ? 'wide' : 'normal',
title: img.title
}));
if (gridRef.value) {
(gridRef.value as any).items = items.value;
}
loading.value = false;
} catch (error) {
console.error('Failed to load images:', error);
loading.value = false;
}
});
</script>
useRef to get a reference to the custom elementitems property in a useEffect hookaddEventListener in useEffectref in Composition API or $refs in Options API@event-name in templates or addEventListener in script'mosaic-grid-widget' to register the custom element'mosaic-grid-widget/types' for TypeScript supportitems property on the elementThe package uses a modular architecture with separate components for the grid and cards:
MosaicGridWidget (mosaic-grid.ts): The main container component that:
MosaicCard (card.ts): Individual card components that:
Custom Element: <mosaic-grid-widget> is registered as a custom HTML element
Shadow DOM: Styles and markup are encapsulated to prevent conflicts with your page styles
CSS Grid: Uses CSS Grid Layout for responsive, flexible positioning
Type Safety: TypeScript discriminated unions ensure type-safe content definitions
items property populates the grid with tilesThe component uses CSS Grid with auto-fit and minmax for responsive behavior:
The grid automatically adjusts based on screen size:
The component handles different content types through a discriminated union pattern:
Each card can be customized with overlays and actions:
Images are automatically lazy-loaded using the Intersection Observer API. Images start loading when they're within 200px of the viewport, improving initial page load performance. Custom HTML previews (previewHtml or previewRenderer) are rendered immediately and bypass lazy loading.
<mosaic-grid-widget></mosaic-grid-widget>
items (setter)Sets the grid items. Accepts an array of MosaicItem objects.
grid.items = [
{ id: '1', type: 'image', preview: '...', full: '...' },
// ... more items
];
MosaicItemThe base type for all grid items. Uses discriminated unions based on the type field.
type MosaicItem = ImageItem | PdfItem | MarkdownItem | VideoItem | LinkItem | CustomItem
ImageItem{
id: string;
type: 'image';
preview: string; // URL for thumbnail/background (lazy-loaded)
full: string; // URL for full-resolution image
title?: string; // Optional title
layout?: LayoutType; // 'normal' | 'wide' | 'tall' | 'big'
previewHtml?: string; // Optional custom HTML for preview
previewRenderer?: PreviewRenderHandler; // Optional function to generate preview HTML
}
PdfItem{
id: string;
type: 'pdf';
preview: string; // URL for PDF thumbnail
src: string; // URL to PDF file
title?: string;
layout?: LayoutType;
}
MarkdownItem{
id: string;
type: 'markdown';
preview: string; // URL for markdown thumbnail
src: string; // URL to markdown file
title?: string;
layout?: LayoutType;
}
VideoItem{
id: string;
type: 'video';
preview: string; // URL for video thumbnail
src: string; // URL to video file
title?: string;
layout?: LayoutType;
}
LinkItem{
id: string;
type: 'external_link';
preview: string; // URL for link thumbnail
url: string; // URL to open
title?: string;
layout?: LayoutType;
}
CustomItem{
id: string;
type: 'custom';
preview: string; // Fallback preview URL (for accessibility)
handler: CustomRenderHandler; // Function that returns HTML when tile is clicked
previewHtml?: string; // Optional custom HTML for preview
previewRenderer?: PreviewRenderHandler; // Optional function to generate preview HTML
title?: string;
layout?: LayoutType;
cardOverlays?: CardOverlays; // Optional custom overlays
cardActions?: CardAction[]; // Optional action buttons
}
CardOverlays{
topRight?: OverlayRenderer; // Custom overlay at top-right
topLeft?: OverlayRenderer; // Custom overlay at top-left
bottomRight?: OverlayRenderer; // Custom overlay at bottom-right
bottomLeft?: OverlayRenderer; // Custom overlay at bottom-left
center?: OverlayRenderer; // Custom overlay at center
}
CardAction{
icon?: string; // Optional icon (emoji, SVG, or class name)
label: string; // Accessibility label
onClick: (item: MosaicItem, cardElement: HTMLElement) => void;
position?: OverlayPosition; // Where to place the button (default: 'top-right')
}
OverlayRenderertype OverlayRenderer = (item: MosaicItem, cardElement: HTMLElement) => HTMLElement;
Function that receives the item and card element, returns an HTMLElement to be placed in the overlay position.
LayoutTypetype LayoutType = 'normal' | 'wide' | 'tall' | 'big';
PreviewRenderHandlertype PreviewRenderHandler = (item: MosaicItem) => string;
Synchronous function that returns HTML string for tile preview. Used for custom previews that don't require async operations.
CustomRenderHandlertype CustomRenderHandler = (item: MosaicItem) => Promise<string>;
Async function that returns HTML string when a custom tile is clicked. Used for dynamic content loading.
const imageGallery: MosaicItem[] = [
{
id: 'img1',
type: 'image',
preview: '/thumbnails/photo1-thumb.jpg',
full: '/photos/photo1-full.jpg',
layout: 'big',
title: 'Sunset over mountains'
},
{
id: 'img2',
type: 'image',
preview: '/thumbnails/photo2-thumb.jpg',
full: '/photos/image2-full.jpg',
layout: 'wide',
title: 'Ocean waves'
}
];
const customTile: MosaicItem = {
id: 'custom1',
type: 'image',
preview: 'data:image/svg+xml,<svg>...</svg>', // Fallback
full: 'https://example.com/image.jpg',
previewHtml: `
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
<svg width="64" height="64"><circle cx="32" cy="32" r="24" fill="white"/></svg>
</div>
`,
layout: 'normal'
};
const customRendererTile: MosaicItem = {
id: 'custom2',
type: 'image',
preview: 'fallback.jpg',
full: 'https://example.com/image.jpg',
previewRenderer: (item) => {
// Generate custom HTML based on item properties
return `<div style="background: radial-gradient(circle, ${item.id === 'custom2' ? '#ff6b6b' : '#4ecdc4'});
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;">
<span style="color: white; font-size: 24px;">Custom</span>
</div>`;
},
layout: 'normal'
};
const customContentTile: CustomItem = {
id: 'custom3',
type: 'custom',
preview: 'placeholder.jpg',
handler: async (item) => {
// Fetch or generate content dynamically
const response = await fetch('https://api.example.com/content');
const data = await response.json();
return `<div class="custom-content">${data.html}</div>`;
},
previewHtml: '<div style="background: #667eea; color: white; padding: 20px;">Click to load</div>',
layout: 'normal'
};
See the live demo at /custom-card.html for a complete working example!
// Helper function to create dropdown menu
function createDropdownMenu(item: MosaicItem, cardElement: HTMLElement): HTMLElement {
const container = document.createElement('div');
container.className = 'dropdown-container';
// Menu button (three dots icon)
const button = document.createElement('button');
button.innerHTML = '⋮';
button.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent card expansion
menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
});
// Dropdown menu
const menu = document.createElement('div');
menu.className = 'dropdown-menu';
menu.style.display = 'none';
// Menu items
const items = [
{ label: 'Edit', action: () => console.log('Edit', item.id) },
{ label: 'Share', action: () => console.log('Share', item.id) },
{ label: 'Delete', action: () => console.log('Delete', item.id) },
];
items.forEach(item => {
const menuItem = document.createElement('button');
menuItem.textContent = item.label;
menuItem.addEventListener('click', (e) => {
e.stopPropagation();
item.action();
menu.style.display = 'none';
});
menu.appendChild(menuItem);
});
container.appendChild(button);
container.appendChild(menu);
return container;
}
// Use in your item
const itemWithDropdown: ImageItem = {
id: 'img-with-menu',
type: 'image',
preview: 'thumb.jpg',
full: 'full.jpg',
cardOverlays: {
topRight: createDropdownMenu
}
};
const itemWithActions: ImageItem = {
id: 'img-with-actions',
type: 'image',
preview: 'thumb.jpg',
full: 'full.jpg',
cardActions: [
{
icon: '✏️',
label: 'Edit',
position: 'top-right',
onClick: (item, cardElement) => {
console.log('Edit clicked for', item.id);
// Open edit modal, etc.
}
},
{
icon: '🗑️',
label: 'Delete',
position: 'top-right',
onClick: (item, cardElement) => {
if (confirm('Delete this item?')) {
// Delete logic
}
}
}
]
};
const mixedContent: MosaicItem[] = [
{
id: 'doc1',
type: 'pdf',
preview: '/thumbnails/report-thumb.png',
src: '/documents/annual-report.pdf',
layout: 'tall',
title: 'Annual Report 2024'
},
{
id: 'readme',
type: 'markdown',
preview: '/thumbnails/readme-thumb.png',
src: 'https://raw.githubusercontent.com/user/repo/main/README.md',
layout: 'normal',
title: 'Project README'
},
{
id: 'video1',
type: 'video',
preview: '/thumbnails/video-thumb.jpg',
src: '/videos/demo.mp4',
layout: 'wide',
title: 'Product Demo'
},
{
id: 'link1',
type: 'external_link',
preview: '/thumbnails/external-thumb.png',
url: 'https://example.com',
layout: 'normal',
title: 'Visit Website'
}
];
The component uses Shadow DOM, so styles are encapsulated. However, you can style the component's container:
mosaic-grid-widget {
display: block;
width: 100%;
max-width: 1200px;
margin: 0 auto;
}
The internal grid uses:
npm run build
Builds the package to dist/ directory with both ES module and UMD formats.
npm run dev
Starts Vite dev server with the demo page.
npm test
Runs Vitest test suite with jsdom environment.
This project uses Vitest with jsdom to test the web component without needing a real browser or Playwright. Here's how it works:
1. jsdom Environment
2. Shadow DOM Testing
element.shadowRoot3. Mock IntersectionObserver
4. Event Simulation
element.click(), element.dispatchEvent(), etc.requestAnimationFrame is available in jsdom, allowing us to test async DOM updatesawait with requestAnimationFrame to ensure DOM updates complete5. What We Test
6. Test Structure
tests/
├── card.test.ts # Unit tests for MosaicCard class
├── mosaic-grid.test.ts # Integration tests for MosaicGridWidget
Benefits of This Approach:
Limitations:
For visual regression testing or browser-specific testing, you could add Playwright/Cypress as an additional test layer, but for component logic and behavior, jsdom is sufficient and much faster.
mosaic-grids/
├── src/
│ ├── mosaic-grid.ts # Main grid component (MosaicGridWidget)
│ ├── card.ts # Card component (MosaicCard)
│ └── types.ts # TypeScript type definitions
├── demo/
│ ├── index.html # Demo page
│ └── main.ts # Demo implementation
├── tests/
│ ├── card.test.ts # Card component tests
│ └── mosaic-grid.test.ts # Grid integration tests
├── dist/ # Built files
└── package.json
The component uses Shadow DOM with mode: 'open' to:
The component includes several performance optimizations:
translateZ(0) to trigger hardware accelerationcontain: layout style paint for rendering isolationThe grid component maintains internal state:
_items: Array of grid items_state: Current grid state ('idle' | 'item-expanded' | 'loading')expandedCard: Reference to currently expanded card instancecards: Map of card instances by item IDimageLoadCache: Shared cache for preloaded images across all cardspreloadedImages: Map of preloaded images by card elementEach card maintains its own state:
isExpanded: Whether the card is currently expandedoverlayElements: Map of overlay elements by positionAnimations use CSS transitions with cubic-bezier easing:
TypeScript discriminated unions ensure:
type fieldmarked or markdown-it.Contributions are welcome! Please ensure:
npm test)# Run tests once
npm test
# Run tests in watch mode
npm test -- --watch
# Run tests with coverage
npm test -- --coverage
When adding new features:
tests/card.test.ts for card-specific functionalitytests/mosaic-grid.test.ts for grid-level behaviorrequestAnimationFrame for async DOM updates in testsMIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
FAQs
A framework-agnostic web component for creating beautiful, animated mosaic-style content grids with lazy loading and custom previews
The npm package mosaic-grid-widget receives a total of 628 weekly downloads. As such, mosaic-grid-widget popularity was classified as not popular.
We found that mosaic-grid-widget demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
PyPI adds Trusted Publishing support for GitLab Self-Managed as adoption reaches 25% of uploads

Research
/Security News
A malicious Chrome extension posing as an Ethereum wallet steals seed phrases by encoding them into Sui transactions, enabling full wallet takeover.

Security News
Socket is heading to London! Stop by our booth or schedule a meeting to see what we've been working on.