Ratio Preserver
A component that maintains aspect ratio and scales content to fit any viewport size while preserving proportions.
Features
- Responsive scaling: Automatically scales content to fit available space
- Aspect ratio preservation: Maintains exact width/height proportions
- Viewport adaptation: Works seamlessly across all device sizes
- Transform-based scaling: Uses CSS transforms for smooth scaling
- Context-aware: Provides scaling information to child components
Installation
Copy and paste the following code into your project.
'use client';
import { cn } from '@kit/shared';
import { Slot } from '@kit/ui/slot';
import * as React from 'react';
/* -------------------------------------------------------------------------------------------------
* RatioPreserver Context
* -----------------------------------------------------------------------------------------------*/
interface RatioPreserverContextValue {
scale: number;
contentWidth: number;
contentHeight: number;
}
const RatioPreserverContext = React.createContext<RatioPreserverContextValue | undefined>(undefined);
const useRatioPreserverContext = () => {
const context = React.useContext(RatioPreserverContext);
if (!context) {
throw new Error('useRatioPreserverContext must be used within RatioPreserver');
}
return context;
};
/* -------------------------------------------------------------------------------------------------
* RatioPreserver Root
* -----------------------------------------------------------------------------------------------*/
export interface RatioPreserverProps {
width: number;
height: number;
children: React.ReactNode;
className?: string;
asChild?: boolean
}
export function RatioPreserver({ width, height, children, className, asChild = false }: RatioPreserverProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
const [scale, setScale] = React.useState(1);
const Comp = asChild ? Slot : 'div'
React.useEffect(() => {
const container = containerRef.current;
if (!container) return;
const updateScale = () => {
const containerHeight = container.clientHeight;
const containerWidth = container.clientWidth;
// Guard against zero-sized container
if (containerWidth === 0 || width === 0 || height === 0) {
setScale(1);
return;
}
// Calculate scale based on available width only
// Container height will be set to match scaled content height
const widthScale = containerWidth / width;
const heightScale = containerHeight / height;
setScale(Math.min(widthScale, heightScale));
};
// Use requestAnimationFrame to ensure layout is complete
const rafId = requestAnimationFrame(() => {
updateScale();
});
// Add window resize listener as backup
const handleWindowResize = () => {
requestAnimationFrame(updateScale);
};
window.addEventListener('resize', handleWindowResize);
updateScale();
// Observe container size changes
// const resizeObserver = new ResizeObserver(() => {
// requestAnimationFrame(updateScale);
// });
// resizeObserver.observe(container);
return () => {
// resizeObserver.disconnect();
cancelAnimationFrame(rafId);
window.removeEventListener('resize', handleWindowResize);
};
}, [width, height]);
const value = React.useMemo(
() => ({
scale,
contentWidth: width,
contentHeight: height,
}),
[scale, width, height]
);
return (
<RatioPreserverContext.Provider value={value}>
<Comp
ref={containerRef}
className={cn('relative', className)}
data-slot="ratio-preserver-container"
style={{
paddingTop: `${(height / width) * 100}%`,
}}
>
{children}
</Comp>
</RatioPreserverContext.Provider>
);
}
/* -------------------------------------------------------------------------------------------------
* RatioPreserverContent
* -----------------------------------------------------------------------------------------------*/
export interface RatioPreserverContentProps {
children: React.ReactNode;
className?: string;
}
export const RatioPreserverContent = React.forwardRef<HTMLDivElement, RatioPreserverContentProps>(
({ children, className }, ref) => {
const { scale, contentWidth, contentHeight } = useRatioPreserverContext();
return (
<div
ref={ref}
className={cn('absolute', className)}
data-slot="ratio-preserver-content"
style={{
width: contentWidth,
height: contentHeight,
top: '50%',
left: '50%',
transform: `translate(-50%, -50%) scale(${scale})`,
transformOrigin: 'center center',
}}
>
{children}
</div>
);
}
);
RatioPreserverContent.displayName = 'RatioPreserverContent';
Update the import paths to match your project setup.
Usage
import { RatioPreserver, RatioPreserverContent } from '@kit/ui/ratio-preserver';<RatioPreserver width={720} height={1080}>
<RatioPreserverContent>
<img
src="https://picsum.photos/720/1080"
alt="Example"
className="w-full h-full object-cover"
/>
</RatioPreserverContent>
</RatioPreserver>Examples
Device Mockup Integration
Perfect integration with MutableDeviceMockupRoot for responsive device previews:
Device scales perfectly on any screen size
410×759px device mockup with responsive scaling
Perfect for showcasing mobile apps on any screen
Device Mockup Integration
This component is commonly used with the MutableDeviceMockupRoot component to ensure device mockups look great on small screens. The scaling ensures the mockup remains readable and proportional regardless of the viewport size.
How It Works
The RatioPreserver component works by:
- Setting container aspect ratio using
padding-toppercentage - Calculating optimal scale based on available width and height
- Applying CSS transform to scale content proportionally
- Centering content using absolute positioning and transforms
The scaling calculation ensures content fits within the container while maintaining the exact aspect ratio specified.
API Reference
RatioPreserver
| Prop | Type | Default |
|---|---|---|
width* | number | |
height* | number | |
children* | React.ReactNode | |
className | string | |
asChild | boolean |
RatioPreserverContent
| Prop | Type | Default |
|---|---|---|
children* | React.ReactNode | |
className | string |
Accessibility
- The component preserves all accessibility features of its children
- Scaling does not affect screen reader navigation
- Focus management remains intact during scaling
- Interactive elements maintain their functionality
Performance
- Uses CSS transforms for hardware-accelerated scaling
- Optimized with
requestAnimationFramefor smooth updates - Minimal re-renders with efficient context usage
- Scales content without affecting layout calculations
Avoid form redundancy by using a quick form component.
Loading placeholder component with shimmer animations.
How is this guide?
Last updated on 11/26/2025