Aspect Ratio
A fluid box that holds a ratio and springs smoothly to a new shape when the ratio changes.
Video · 16:9
Installation
pnpm dlx shadcn-vue@latest add https://vuelix.dev/r/aspect-ratio.jsonManual installation
Install the dependencies:
bash
npm install motion-vThen copy these files into components/ui/aspect-ratio/ (plus lib/utils.ts if you don't have it):
vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { motion, useReducedMotion, useSpring, useTransform } from 'motion-v'
import { watch } from 'vue'
import { cn } from '@/lib/utils'
const props = withDefaults(defineProps<{
/** Width divided by height, e.g. `16 / 9`. Changing it animates the box. */
ratio?: number
/** Optional image rendered edge to edge inside the box. */
src?: string
alt?: string
/** How the image fills the box (`object-fit`). */
fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'
/** Which part of the image stays visible when cropped (`object-position`). */
position?: string
class?: HTMLAttributes['class']
}>(), {
ratio: 16 / 9,
alt: '',
fit: 'cover',
position: 'center',
})
const reduced = useReducedMotion()
// The ratio itself is the animated value, so the box stays fluid in width
// and never needs to be measured.
const ratio = useSpring(props.ratio, { stiffness: 220, damping: 26, mass: 0.9 })
const aspectRatio = useTransform(ratio, r => `${Math.max(r, 0.01)}`)
watch(() => props.ratio, (next) => {
if (reduced.value)
ratio.jump(next)
else
ratio.set(next)
})
</script>
<template>
<motion.div
:class="cn('relative w-full overflow-hidden rounded-xl bg-muted', props.class)"
:style="{ aspectRatio }"
>
<img
v-if="props.src"
:src="props.src"
:alt="props.alt"
class="absolute inset-0 size-full"
:style="{ objectFit: props.fit, objectPosition: props.position }"
draggable="false"
>
<div v-if="$slots.default" class="absolute inset-0">
<slot />
</div>
</motion.div>
</template>ts
export { default as AspectRatio } from './AspectRatio.vue'Usage
ts
import { AspectRatio } from '@/components/ui/aspect-ratio'vue
<AspectRatio :ratio="16 / 9" src="/cover.jpg" alt="Cover" position="top" />
<!-- or any content -->
<AspectRatio :ratio="1">
<video src="/clip.mp4" class="size-full object-cover" autoplay muted loop />
</AspectRatio>API
| Prop | Type | Default | Description |
|---|---|---|---|
ratio | number | 16 / 9 | Width divided by height. Changing it springs the box to the new shape. |
src | string | — | Optional image rendered edge to edge. |
alt | string | '' | Alt text for src. |
fit | 'cover' | 'contain' | 'fill' | 'none' | 'scale-down' | 'cover' | object-fit for the image. |
position | string | 'center' | object-position for the image, e.g. 'top' or '30% 70%'. |
The default slot fills the box (absolutely positioned) and is layered over the image, so it doubles as an overlay. The ratio itself is the animated value, so the box stays fluid in width and is never measured; with prefers-reduced-motion it snaps.