Display
Aspect Video Frame
An embed frame with a fixed aspect ratio: the space for the video is reserved before the iframe loads, so the page never jumps.
- frame
- video
- embed
- aspect-ratio
Preview
Use it with AI
- 1. Copy the link.
- 2. Write to your agent in your own words and drop the link into the sentence.
- 3. The agent opens the link and installs the component from the registry.
put this in the header: https://vibeui.ru/c/frame-002?lang=en
Show the full instructions
What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.
# Install and place "frame-002" (Aspect Video Frame) from VibeUI
## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/frame-002.json
Registry item: https://vibeui.ru/r/frame-002.json
Installs to: components/vibeui/frame-002.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.
Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.
## 2. What it is
An embed frame with a fixed aspect ratio: the space for the video is reserved before the iframe loads, so the page never jumps.
A video embed frame: the aspect ratio reserves space before loading and the nested iframe stretches to fill the frame. Zero dependencies, one file, no client JS.
## 3. How to use it
import { Frame002 } from "@/components/vibeui/frame-002"
<Frame002 ratio="16:9" title="VibeUI catalog tour">
<iframe src="https://example.com/embed/abc" title="Tour" />
</Frame002>
Read the installed file for the full prop list.
## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.
Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)
## 5. Keep exactly as installed
- the local --vibeui-frame-002-* palette — do not swap it for your theme tokens
- the aspect ratio on the frame itself: without it the page jumps the moment the video loads
- stretching the nested iframe to inset:0 — otherwise it stays 150px tall and breaks the ratio
- overflow:hidden on the frame: video players carry their own rounded corners that spill outside
- the stub with the play triangle: an empty dark rectangle reads as a loading error
- the component's own light surface: without it the dark screen merges into a dark catalog background
## 6. You may change
- the proportion through ratio: 16:9 for clips, 1:1 for stories, 21:9 for trailers
- the stub caption through title
- the text under the frame through caption
- the frame width through max-width
## 7. Rules
- aspect-ratio is supported by every current browser, but in very old engines the frame collapses to zero height.
- object-fit:cover crops the edges: switch it to contain when the whole frame matters.
- Exactly one media node belongs inside — the frame cannot help two absolutely positioned children.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.
## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.For developers
npx shadcn@latest add https://vibeui.ru/r/frame-002.jsonhttps://vibeui.ru/r/frame-002.jsonСерверный компонент, хуков нет. Соотношение сторон объявлено переменной --vibeui-frame-002-ratio на самой рамке, а варианты 4:3, 1:1 и 21:9 переопределяют её через атрибут data-ratio — так пропорция задаётся одной строкой, а не набором классов. Вложенный iframe, img или video растягиваются правилом для :where(iframe,img,video) на inset:0: без него iframe держит собственные 150px высоты и пропорция рамки перестаёт что-либо значить. Заглушка с треугольником нарисована рамками через ::before, чтобы не тащить svg. Компонент несёт свою белую подложку — тёмный экран внутри белой карточки читается на любом фоне каталога.
Component source
The same file your agent installs. Here in case you would rather copy it by hand.
import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react"
export type Frame002Props = Omit<
ComponentPropsWithoutRef<"figure">,
"title"
> & {
ratio?: "16:9" | "4:3" | "1:1" | "21:9"
title?: string
caption?: string
children?: ReactNode
}
// Идея компонента: рамка встраивания держит пропорцию до загрузки контента.
// Место под ролик резервируется соотношением сторон на самой рамке, поэтому
// страница не прыгает, когда iframe наконец приходит. Вложенный iframe
// растягивается абсолютом: без этого он оставляет свои дефолтные 150px
// высоты и пропорция рамки перестаёт что-либо значить.
const STYLES = `
:where([data-vibeui-block="frame-002"]){
--vibeui-frame-002-ratio:16 / 9;
--vibeui-frame-002-bg:oklch(1 0 0);
--vibeui-frame-002-screen:oklch(0.22 0.02 265);
--vibeui-frame-002-fg:oklch(0.24 0.014 265);
--vibeui-frame-002-muted:oklch(0.56 0.014 265);
--vibeui-frame-002-border:oklch(0.9 0.006 265);
--vibeui-frame-002-light:oklch(0.98 0.002 265);
--vibeui-frame-002-radius:0.75rem;
--vibeui-frame-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="frame-002"]{
display:flex;flex-direction:column;gap:0.625rem;margin:0;
width:100%;max-width:32rem;box-sizing:border-box;padding:0.625rem;
background:var(--vibeui-frame-002-bg);
border:1px solid var(--vibeui-frame-002-border);
border-radius:calc(var(--vibeui-frame-002-radius) + 0.25rem);
font-family:var(--vibeui-frame-002-font);color:var(--vibeui-frame-002-fg);
}
[data-vibeui-block="frame-002"] *{box-sizing:border-box}
/* Соотношение сторон живёт на рамке: место под ролик занято до загрузки. */
[data-vibeui-block="frame-002"] [data-part="shell"]{
position:relative;overflow:hidden;
aspect-ratio:var(--vibeui-frame-002-ratio);
border-radius:var(--vibeui-frame-002-radius);
background:var(--vibeui-frame-002-screen);
}
/* Встраиваемый узел растягивается абсолютом: iframe иначе держит свои 150px. */
[data-vibeui-block="frame-002"] [data-part="shell"] > :where(iframe,img,video){
position:absolute;inset:0;display:block;width:100%;height:100%;border:0;
object-fit:cover;
}
[data-vibeui-block="frame-002"] [data-part="stub"]{
position:absolute;inset:0;
display:grid;place-content:center;justify-items:center;gap:0.625rem;
padding:1rem;text-align:center;color:var(--vibeui-frame-002-light);
}
[data-vibeui-block="frame-002"] [data-part="play"]{
display:grid;place-items:center;
width:3rem;height:3rem;border-radius:9999px;
background:color-mix(in oklab,var(--vibeui-frame-002-light) 16%,transparent);
border:1px solid color-mix(in oklab,var(--vibeui-frame-002-light) 40%,transparent);
}
[data-vibeui-block="frame-002"] [data-part="play"]::before{
content:"";width:0;height:0;margin-left:0.2rem;
border-left:0.75rem solid var(--vibeui-frame-002-light);
border-top:0.5rem solid transparent;border-bottom:0.5rem solid transparent;
}
[data-vibeui-block="frame-002"] [data-part="stub-title"]{font-size:0.8125rem;opacity:.85}
[data-vibeui-block="frame-002"] [data-part="badge"]{
position:absolute;right:0.5rem;bottom:0.5rem;
padding:0.125rem 0.375rem;border-radius:0.375rem;
background:color-mix(in oklab,var(--vibeui-frame-002-screen) 65%,transparent);
color:var(--vibeui-frame-002-light);
font-size:0.6875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="frame-002"] figcaption{
font-size:0.75rem;line-height:1.4;color:var(--vibeui-frame-002-muted);
}
[data-vibeui-block="frame-002"][data-ratio="4:3"]{--vibeui-frame-002-ratio:4 / 3}
[data-vibeui-block="frame-002"][data-ratio="1:1"]{--vibeui-frame-002-ratio:1 / 1}
[data-vibeui-block="frame-002"][data-ratio="21:9"]{--vibeui-frame-002-ratio:21 / 9}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="frame-002"] *{animation:none!important;transition:none!important}}
`
/**
* Рамка встраивания с фиксированным соотношением сторон.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Frame002({
ratio = "16:9",
title = "Обзор каталога VibeUI",
caption = "Ролик подгружается — место под него уже занято",
children,
className,
style,
...props
}: Frame002Props) {
return (
<>
<style href="vibeui-frame-002" precedence="medium">
{STYLES}
</style>
<figure
{...props}
data-vibeui-block="frame-002"
data-ratio={ratio}
className={className}
style={style as CSSProperties}
>
<div data-part="shell">
{children ?? (
<div data-part="stub">
<span data-part="play" aria-hidden="true" />
<span data-part="stub-title">{title}</span>
</div>
)}
<span data-part="badge">{ratio}</span>
</div>
{caption ? <figcaption>{caption}</figcaption> : null}
</figure>
</>
)
}