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

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 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

VibeUI catalog tour
16:9
Ролик подгружается — место под него уже занято
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.json
https://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>
    </>
  )
}