Let’s build a lightweight, high-performance mobile app showcase component that mimics top-tier design galleries using React and Tailwind CSS.
What you'll learn
- Structuring a modular device frame wrapper for mobile web previews.
- Implementing low-overhead scroll parallax using vanilla React hooks.
- Handling touch and pointer interactions for interactive app screens.
- Optimizing layout shifts when loading responsive preview assets.
Setting Up the Device Frame Wrapper
When showcasing mobile apps on the web, your presentation container needs to mimic real hardware without killing performance with heavy SVGs or complex shadows. We start by building a clean container component that acts as our hardware shell.
import React from 'react';
interface DeviceFrameProps {
children: React.ReactNode;
title?: string;
className?: string;
}
export const DeviceFrame: React.FC<DeviceFrameProps> = ({
children,
title = "App Preview",
className = ""
}) => {
return (
<div className={`relative mx-auto w-[320px] h-[650px] bg-neutral-900 rounded-[40px] p-3 shadow-2xl border-4 border-neutral-800 ${className}`}>
{/* Speaker notch */}
<div className="absolute top-5 left-1/2 -translate-x-1/2 w-28 h-4 bg-black rounded-full z-20 flex items-center justify-center">
<div className="w-12 h-1 bg-neutral-800 rounded-full" />
</div>
{/* Screen viewport */}
<div className="relative w-full h-full bg-white rounded-[32px] overflow-hidden flex flex-col pt-8">
<div className="sr-only">{title}</div>
{children}
</div>
</div>
);
};
Speed Note: Avoid heavy CSS filter drop-shadows on mobile viewports. Stick to Tailwind's hardware-accelerated
shadow-2xlcombined with crisp borders to keep paint times low during rapid scrolling.
Adding Scroll Parallax to Showcase Grids
To give your app showcase that polished, staggered feel, we can track window scroll coordinates and apply minor translate offsets to alternating grid columns.
import React, { useEffect, useState } from 'react';
export const useParallaxOffset = (speed: number = 0.15): number => {
const [offset, setOffset] = useState(0);
useEffect(() => {
const handleScroll = () => {
// requestAnimationFrame prevents layout thrashing
window.requestAnimationFrame(() => {
setOffset(window.scrollY * speed);
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, [speed]);
return offset;
};
Gotcha: Always register scroll listeners with
{ passive: true }. This tells the browser you won't callpreventDefault(), allowing it to run scroll animations smoothly without blocking the main thread.
Composing the Interactive Grid Showcase
Now we combine our device frame and parallax hook into a responsive multi-column showcase grid. This layout adapts seamlessly from single-column mobile views to a staggered three-column desktop showcase.
import React from 'react';
import { DeviceFrame } from './DeviceFrame';
import { useParallaxOffset } from './useParallaxOffset';
interface AppItem {
id: string;
name: string;
category: string;
imageSrc: string;
}
const SAMPLE_APPS: AppItem[] = [
{ id: '1', name: 'Reel Motion', category: 'Video / UI', imageSrc: 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?auto=format&fit=crop&w=600&q=80' },
{ id: '2', name: 'Glyphor PRO', category: 'Productivity', imageSrc: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=600&q=80' },
{ id: '3', name: 'übernatural', category: 'Lifestyle', imageSrc: 'https://images.unsplash.com/photo-1507238691740-187a5b1d37b8?auto=format&fit=crop&w=600&q=80' },
];
export const AppShowcaseGrid: React.FC = () => {
const parallaxOffset = useParallaxOffset(0.1);
return (
<div className="max-w-7xl mx-auto px-4 py-16">
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{SAMPLE_APPS.map((app, index) => {
// Stagger parallax direction for alternating columns
const directionMultiplier = index % 2 === 0 ? 1 : -1;
const dynamicStyle = {
transform: `translateY(${parallaxOffset * directionMultiplier}px)`,
transition: 'transform 0.1s cubic-bezier(0,0,0.2,1)'
};
return (
<div key={app.id} style={dynamicStyle} className="flex flex-col items-center">
<DeviceFrame title={app.name}>
<img
src={app.imageSrc}
alt={app.name}
className="w-full h-full object-cover"
loading="lazy"
/>
</DeviceFrame>
<div className="mt-4 text-center">
<h3 className="text-white font-semibold text-lg">{app.name}</h3>
<p className="text-neutral-400 text-sm">{app.category}</p>
</div>
</div>
);
})}
</div>
</div>
);
};
Accessibility: Notice how we pass meaningful alt text down to each app preview image and maintain descriptive headings. Screen reader users can easily parse the showcase items even with complex visual positioning.
Handling Touch Previews and Hover States
To mimic high-end design galleries, users should be able to hover over a device frame to trigger a subtle scale increase and shadow elevation.
import React, { useState } from 'react';
interface InteractiveWrapperProps {
children: React.ReactNode;
}
export const InteractiveWrapper: React.FC<InteractiveWrapperProps> = ({ children }) => {
const [isHovered, setIsHovered] = useState(false);
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`transition-transform duration-300 ease-out cursor-pointer ${
isHovered ? '-translate-y-2 scale-[1.02]' : 'translate-y-0 scale-100'
}`}
>
{children}
</div>
);
};
Speed Note: Restrict transitions to
transformandopacity. Animate properties likewidth,height, ortoptriggers costly layout recalculations on every frame.
Summary
Building a custom mobile app showcase doesn't require bloated UI libraries. By combining a clean hardware frame component, passive scroll hooks, and performant Tailwind transforms, you get a buttery-smooth portfolio grid.
Next Steps
- Add keyboard navigation support so users can tab through different showcase frames.
- Integrate Intersection Observer to lazy-load heavy video mockups only when they enter the viewport.
- Pair this layout with a headless CMS to dynamically populate app showcases from your backend.