UI Components Easy
Aspect Ratio
A container component that maintains a specific aspect ratio regardless of content or viewport size. Uses the modern CSS aspect-ratio property with a padding-bottom fallback.
Open in Lab
MCP
css javascript vue svelte
Targets: TS JS HTML React Vue Svelte
Code
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, sans-serif;
background: #0a0a0a;
color: #f1f5f9;
min-height: 100vh;
display: grid;
place-items: center;
padding: 2rem;
}
.ar-wrapper {
width: min(700px, 100%);
display: flex;
flex-direction: column;
gap: 1rem;
}
.ar-title {
font-size: 1.375rem;
font-weight: 700;
color: #f1f5f9;
}
.ar-subtitle {
font-size: 0.875rem;
color: #64748b;
margin-bottom: 0.5rem;
}
.ar-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
align-items: start;
}
.ar-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.ar-card--wide {
grid-column: span 2;
}
.ar-badge {
font-size: 0.7rem;
font-weight: 600;
color: #94a3b8;
letter-spacing: 0.06em;
font-family: "Fira Code", monospace;
}
/* ── Aspect Ratio container ── */
.aspect-ratio {
position: relative;
width: 100%;
aspect-ratio: var(--ar-w) / var(--ar-h);
border-radius: 0.75rem;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.08);
}
/* Fallback for browsers without aspect-ratio support */
@supports not (aspect-ratio: 1) {
.aspect-ratio {
height: 0;
padding-bottom: calc(var(--ar-h) / var(--ar-w) * 100%);
}
.aspect-ratio > * {
position: absolute;
inset: 0;
}
}
/* ── Content inside ── */
.ar-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
color: rgba(255, 255, 255, 0.6);
}
.ar-label {
font-size: 0.8rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.4);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ── Gradient backgrounds ── */
.ar-content--gradient-1 {
background: linear-gradient(135deg, #1e1b4b, #312e81);
}
.ar-content--gradient-2 {
background: linear-gradient(135deg, #0c4a6e, #164e63);
}
.ar-content--gradient-3 {
background: linear-gradient(135deg, #14532d, #1a2e05);
}
.ar-content--gradient-4 {
background: linear-gradient(135deg, #4c1d95, #701a75);
}
.ar-content--gradient-5 {
background: linear-gradient(135deg, #78350f, #7c2d12);
}
/* ── Responsive ── */
@media (max-width: 480px) {
.ar-grid {
grid-template-columns: 1fr 1fr;
}
.ar-card--wide {
grid-column: span 2;
}
}(function () {
"use strict";
// The aspect-ratio component is primarily CSS-driven.
// This script adds interactive ratio switching for demonstration purposes.
var containers = document.querySelectorAll(".aspect-ratio");
// Feature detection: check if aspect-ratio is supported
var supportsAspectRatio = CSS.supports("aspect-ratio", "1");
if (!supportsAspectRatio) {
// Apply padding-bottom fallback manually
containers.forEach(function (el) {
var w = parseFloat(getComputedStyle(el).getPropertyValue("--ar-w")) || 16;
var h = parseFloat(getComputedStyle(el).getPropertyValue("--ar-h")) || 9;
el.style.height = "0";
el.style.paddingBottom = (h / w) * 100 + "%";
var child = el.firstElementChild;
if (child) {
child.style.position = "absolute";
child.style.inset = "0";
}
});
}
// Log container dimensions for debugging
containers.forEach(function (el) {
var observer = new ResizeObserver(function (entries) {
entries.forEach(function (entry) {
var w = Math.round(entry.contentRect.width);
var h = Math.round(entry.contentRect.height);
el.setAttribute("title", w + " x " + h + "px");
});
});
observer.observe(el);
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Aspect Ratio</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="ar-wrapper">
<h2 class="ar-title">Aspect Ratio Containers</h2>
<p class="ar-subtitle">Resize the browser to see them maintain their proportions</p>
<div class="ar-grid">
<!-- 16:9 -->
<div class="ar-card">
<span class="ar-badge">16 : 9</span>
<div class="aspect-ratio" style="--ar-w: 16; --ar-h: 9;">
<div class="ar-content ar-content--gradient-1">
<span class="ar-label">Widescreen</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
<rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/>
</svg>
</div>
</div>
</div>
<!-- 4:3 -->
<div class="ar-card">
<span class="ar-badge">4 : 3</span>
<div class="aspect-ratio" style="--ar-w: 4; --ar-h: 3;">
<div class="ar-content ar-content--gradient-2">
<span class="ar-label">Classic</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
<rect x="3" y="3" width="18" height="18" rx="3"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="m21 15-5-5L5 21"/>
</svg>
</div>
</div>
</div>
<!-- 1:1 -->
<div class="ar-card">
<span class="ar-badge">1 : 1</span>
<div class="aspect-ratio" style="--ar-w: 1; --ar-h: 1;">
<div class="ar-content ar-content--gradient-3">
<span class="ar-label">Square</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>
</svg>
</div>
</div>
</div>
<!-- 21:9 -->
<div class="ar-card ar-card--wide">
<span class="ar-badge">21 : 9</span>
<div class="aspect-ratio" style="--ar-w: 21; --ar-h: 9;">
<div class="ar-content ar-content--gradient-4">
<span class="ar-label">Ultra-Wide</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
<path d="M15 10l5 5-5 5"/><path d="M4 4v7a4 4 0 0 0 4 4h12"/>
</svg>
</div>
</div>
</div>
<!-- 9:16 -->
<div class="ar-card">
<span class="ar-badge">9 : 16</span>
<div class="aspect-ratio" style="--ar-w: 9; --ar-h: 16;">
<div class="ar-content ar-content--gradient-5">
<span class="ar-label">Portrait</span>
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.5">
<rect x="5" y="2" width="14" height="20" rx="3"/><path d="M12 18h.01"/>
</svg>
</div>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { CSSProperties, ReactNode } from "react";
interface AspectRatioProps {
ratio?: number;
children?: ReactNode;
className?: string;
style?: CSSProperties;
}
function AspectRatioBox({ ratio = 16 / 9, children, style }: AspectRatioProps) {
return (
<div
style={{
position: "relative",
width: "100%",
aspectRatio: String(ratio),
borderRadius: "0.75rem",
overflow: "hidden",
border: "1px solid rgba(255,255,255,0.08)",
...style,
}}
>
{children}
</div>
);
}
function DemoContent({
label,
gradient,
icon,
}: {
label: string;
gradient: string;
icon: ReactNode;
}) {
return (
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
gap: "0.5rem",
background: gradient,
color: "rgba(255,255,255,0.6)",
}}
>
<span
style={{
fontSize: "0.8rem",
fontWeight: 500,
color: "rgba(255,255,255,0.4)",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
{label}
</span>
<div style={{ opacity: 0.5 }}>{icon}</div>
</div>
);
}
const MonitorIcon = (
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</svg>
);
const ImageIcon = (
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<rect x="3" y="3" width="18" height="18" rx="3" />
<circle cx="8.5" cy="8.5" r="1.5" />
<path d="m21 15-5-5L5 21" />
</svg>
);
const CircleIcon = (
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<circle cx="12" cy="12" r="10" />
<path d="m9 12 2 2 4-4" />
</svg>
);
const demos: { label: string; ratio: number; gradient: string; icon: ReactNode; wide?: boolean }[] =
[
{
label: "Widescreen",
ratio: 16 / 9,
gradient: "linear-gradient(135deg, #1e1b4b, #312e81)",
icon: MonitorIcon,
},
{
label: "Classic",
ratio: 4 / 3,
gradient: "linear-gradient(135deg, #0c4a6e, #164e63)",
icon: ImageIcon,
},
{
label: "Square",
ratio: 1,
gradient: "linear-gradient(135deg, #14532d, #1a2e05)",
icon: CircleIcon,
},
{
label: "Ultra-Wide",
ratio: 21 / 9,
gradient: "linear-gradient(135deg, #4c1d95, #701a75)",
icon: MonitorIcon,
wide: true,
},
{
label: "Portrait",
ratio: 9 / 16,
gradient: "linear-gradient(135deg, #78350f, #7c2d12)",
icon: (
<svg
width="40"
height="40"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.5}
>
<rect x="5" y="2" width="14" height="20" rx="3" />
<path d="M12 18h.01" />
</svg>
),
},
];
export default function AspectRatio({ ratio }: { ratio?: number }) {
return (
<div
style={{
minHeight: "100vh",
background: "#0a0a0a",
display: "grid",
placeItems: "center",
padding: "2rem",
fontFamily: "system-ui, -apple-system, sans-serif",
color: "#f1f5f9",
}}
>
<div
style={{ width: "min(700px, 100%)", display: "flex", flexDirection: "column", gap: "1rem" }}
>
<h2 style={{ fontSize: "1.375rem", fontWeight: 700 }}>Aspect Ratio Containers</h2>
<p style={{ fontSize: "0.875rem", color: "#64748b", marginBottom: "0.5rem" }}>
Resize the browser to see them maintain their proportions
</p>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(3, 1fr)",
gap: "1rem",
alignItems: "start",
}}
>
{demos.map((demo) => (
<div
key={demo.label}
style={{
display: "flex",
flexDirection: "column",
gap: "0.5rem",
gridColumn: demo.wide ? "span 2" : undefined,
}}
>
<span
style={{
fontSize: "0.7rem",
fontWeight: 600,
color: "#94a3b8",
letterSpacing: "0.06em",
fontFamily: '"Fira Code", monospace',
}}
>
{Math.round(demo.ratio * 100) / 100 === demo.ratio ? `${demo.ratio}` : demo.label}
</span>
<AspectRatioBox ratio={ratio || demo.ratio}>
<DemoContent label={demo.label} gradient={demo.gradient} icon={demo.icon} />
</AspectRatioBox>
</div>
))}
</div>
</div>
</div>
);
}<script setup>
const demos = [
{ label: "Widescreen", ratio: 16 / 9, gradient: "linear-gradient(135deg,#1e1b4b,#312e81)" },
{ label: "Classic", ratio: 4 / 3, gradient: "linear-gradient(135deg,#0c4a6e,#164e63)" },
{ label: "Square", ratio: 1, gradient: "linear-gradient(135deg,#14532d,#1a2e05)" },
{
label: "Ultra-Wide",
ratio: 21 / 9,
gradient: "linear-gradient(135deg,#4c1d95,#701a75)",
wide: true,
},
{ label: "Portrait", ratio: 9 / 16, gradient: "linear-gradient(135deg,#78350f,#7c2d12)" },
];
</script>
<template>
<div style="min-height:100vh;background:#0a0a0a;display:grid;place-items:center;padding:2rem;font-family:system-ui,-apple-system,sans-serif;color:#f1f5f9">
<div style="width:min(700px,100%);display:flex;flex-direction:column;gap:1rem">
<h2 style="font-size:1.375rem;font-weight:700">Aspect Ratio Containers</h2>
<p style="font-size:0.875rem;color:#64748b;margin-bottom:0.5rem">Resize the browser to see them maintain their proportions</p>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;align-items:start">
<div v-for="demo in demos" :key="demo.label" :style="{ display:'flex', flexDirection:'column', gap:'0.5rem', gridColumn: demo.wide ? 'span 2' : undefined }">
<span style="font-size:0.7rem;font-weight:600;color:#94a3b8;letter-spacing:0.06em;font-family:'Fira Code',monospace">{{ demo.label }}</span>
<div :style="{ position:'relative', width:'100%', aspectRatio: String(demo.ratio), borderRadius:'0.75rem', overflow:'hidden', border:'1px solid rgba(255,255,255,0.08)' }">
<div :style="{ width:'100%', height:'100%', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:'0.5rem', background: demo.gradient, color:'rgba(255,255,255,0.6)' }">
<span style="font-size:0.8rem;font-weight:500;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:0.06em">{{ demo.label }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template><script>
const demos = [
{ label: "Widescreen", ratio: 16 / 9, gradient: "linear-gradient(135deg,#1e1b4b,#312e81)" },
{ label: "Classic", ratio: 4 / 3, gradient: "linear-gradient(135deg,#0c4a6e,#164e63)" },
{ label: "Square", ratio: 1, gradient: "linear-gradient(135deg,#14532d,#1a2e05)" },
{
label: "Ultra-Wide",
ratio: 21 / 9,
gradient: "linear-gradient(135deg,#4c1d95,#701a75)",
wide: true,
},
{ label: "Portrait", ratio: 9 / 16, gradient: "linear-gradient(135deg,#78350f,#7c2d12)" },
];
</script>
<div style="min-height:100vh;background:#0a0a0a;display:grid;place-items:center;padding:2rem;font-family:system-ui,-apple-system,sans-serif;color:#f1f5f9">
<div style="width:min(700px,100%);display:flex;flex-direction:column;gap:1rem">
<h2 style="font-size:1.375rem;font-weight:700">Aspect Ratio Containers</h2>
<p style="font-size:0.875rem;color:#64748b;margin-bottom:0.5rem">Resize the browser to see them maintain their proportions</p>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:1rem;align-items:start">
{#each demos as demo}
<div style="display:flex;flex-direction:column;gap:0.5rem;{demo.wide ? 'grid-column:span 2' : ''}">
<span style="font-size:0.7rem;font-weight:600;color:#94a3b8;letter-spacing:0.06em;font-family:'Fira Code',monospace">
{demo.label}
</span>
<div style="position:relative;width:100%;aspect-ratio:{demo.ratio};border-radius:0.75rem;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
<div style="width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:0.5rem;background:{demo.gradient};color:rgba(255,255,255,0.6)">
<span style="font-size:0.8rem;font-weight:500;color:rgba(255,255,255,0.4);text-transform:uppercase;letter-spacing:0.06em">{demo.label}</span>
</div>
</div>
</div>
{/each}
</div>
</div>
</div>Aspect Ratio
A utility container that enforces a specific aspect ratio on its children. Uses the modern CSS aspect-ratio property with a padding-bottom percentage fallback for older browsers.
How it works
- The container uses
aspect-ratio: <width> / <height>to maintain proportions. - A fallback uses
padding-bottom: calc(height / width * 100%)withposition: absolutechildren for browsers withoutaspect-ratiosupport. - Content inside the container is stretched to fill via
object-fit: cover.
Features
- Native
aspect-ratioCSS property with padding-bottom fallback - Supports any ratio (16:9, 4:3, 1:1, 21:9, etc.)
- Content fills container with
object-fit: cover - Responsive by default