Card

Ratio Media Card

A card with media on top at a locked aspect ratio: the space is reserved before the image loads, so the text below never jumps.

  • card
  • media
  • aspect-ratio
  • grid

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/card-012?lang=en

Видео12:40

Building a catalog with container queries

Разбираем, почему карточка обязана считать раскладку от своей ширины.

Опубликовано вчера

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 "card-012" (Ratio Media Card) 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/card-012.json

Registry item: https://vibeui.ru/r/card-012.json
Installs to: components/vibeui/card-012.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
A card with media on top at a locked aspect ratio: the space is reserved before the image loads, so the text below never jumps.

A card with media on top: a frame at a fixed aspect ratio, a badge and a duration over it, a title and a description below. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Card012 } from "@/components/vibeui/card-012"

<Card012
  ratio="16/9"
  badge="Video"
  duration="12:40"
  title="Building the catalog"
  media={<img src="/cover.jpg" alt="" />}
/>

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-card-012-* palette — do not swap it for your theme tokens
- aspect-ratio on the frame: without it the page jumps as the image arrives, visible on any slow connection
- object-fit:cover on a nested <img>: an image stretched on both axes looks broken
- the badge and duration on opaque backings — white text vanishes over a light frame
- clamping the title and description to two lines: cards in a grid must share one height
- the card's own light surface: dark text on the dark showcase background is unreadable

## 6. You may change
- the title and description copy and the meta content
- the aspect ratio through ratio
- the badge and the duration caption — both drop out on an empty string
- the accent through the accent prop — the placeholder gradient

## 7. Rules
- The frame is empty by default: pass an <img> or <video> as media, the CSS is ready.
- A 1/1 ratio eats half the screen in a narrow column — use 16/9 for a mobile feed.
- The badge covers the top-left corner of the picture: keep important detail out of it.
- 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/card-012.json
https://vibeui.ru/r/card-012.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-card-012-*. Серверный: хуков нет. Кадр держит пропорцию через aspect-ratio, значение приходит из data-ratio и переменной, поэтому четыре соотношения живут без дублирования правил. Плашка и длительность лежат поверх кадра на полупрозрачных подложках, а не рядом с ним. Пустой кадр залит градиентами — картинка не обязательна; переданный <img> получает object-fit:cover и заполняет кадр без искажений.

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 Card012Props = Omit<
  ComponentPropsWithoutRef<"article">,
  "children" | "title"
> & {
  title?: string
  description?: string
  /** Соотношение сторон кадра. Место под медиа резервируется до загрузки. */
  ratio?: "16/9" | "4/3" | "1/1" | "3/2"
  /** Плашка поверх кадра: раздел, тип материала. */
  badge?: string
  /** Подпись в углу кадра: длительность, число слайдов, вес. */
  duration?: string
  meta?: ReactNode
  /** Содержимое кадра: <img>, <video>. Пусто — рисуется градиент-заглушка. */
  media?: ReactNode
  accent?: string
}

// Идея компонента: кадр сверху с жёстким соотношением сторон. Место под
// картинку резервируется через aspect-ratio ещё до её загрузки, поэтому
// текст под кадром не прыгает, а в сетке все карточки одной высоты.
// Плашка и длительность лежат поверх кадра на затемнении, а не рядом с ним.
const STYLES = `
:where([data-vibeui-block="card-012"]){
--vibeui-card-012-bg:oklch(1 0 0);
--vibeui-card-012-fg:oklch(0.22 0.015 265);
--vibeui-card-012-muted:oklch(0.55 0.013 265);
--vibeui-card-012-border:oklch(0.91 0.006 265);
--vibeui-card-012-accent:oklch(0.58 0.19 25);
--vibeui-card-012-radius:0.9375rem;
--vibeui-card-012-ratio:16 / 9;
--vibeui-card-012-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="card-012"]{
display:flex;flex-direction:column;overflow:hidden;
width:100%;max-width:23rem;box-sizing:border-box;
background:var(--vibeui-card-012-bg);color:var(--vibeui-card-012-fg);
border:1px solid var(--vibeui-card-012-border);
border-radius:var(--vibeui-card-012-radius);
font-family:var(--vibeui-card-012-font);
}
[data-vibeui-block="card-012"][data-ratio="4/3"]{--vibeui-card-012-ratio:4 / 3}
[data-vibeui-block="card-012"][data-ratio="1/1"]{--vibeui-card-012-ratio:1 / 1}
[data-vibeui-block="card-012"][data-ratio="3/2"]{--vibeui-card-012-ratio:3 / 2}
/* Кадр держит пропорцию сам: место под картинку есть до её загрузки. */
[data-vibeui-block="card-012"] [data-part="frame"]{
position:relative;aspect-ratio:var(--vibeui-card-012-ratio);
background:
radial-gradient(110% 80% at 20% 0%,color-mix(in oklab,var(--vibeui-card-012-accent) 40%,transparent),transparent 65%),
linear-gradient(155deg,oklch(0.93 0.03 60),oklch(0.86 0.05 25));
}
[data-vibeui-block="card-012"] [data-part="frame"] img,
[data-vibeui-block="card-012"] [data-part="frame"] video{
display:block;width:100%;height:100%;object-fit:cover;
}
[data-vibeui-block="card-012"] [data-part="badge"]{
position:absolute;inset-block-start:0.625rem;inset-inline-start:0.625rem;
display:inline-flex;align-items:center;height:1.375rem;padding:0 0.5rem;
border-radius:9999px;background:oklch(1 0 0 / 92%);
color:var(--vibeui-card-012-fg);font-size:0.6875rem;font-weight:650;
}
[data-vibeui-block="card-012"] [data-part="duration"]{
position:absolute;inset-block-end:0.625rem;inset-inline-end:0.625rem;
display:inline-flex;align-items:center;height:1.25rem;padding:0 0.4375rem;
border-radius:0.375rem;background:oklch(0.2 0.02 265 / 78%);
color:oklch(0.99 0 0);font-size:0.6875rem;font-weight:600;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="card-012"] [data-part="body"]{
display:flex;flex-direction:column;gap:0.375rem;padding:0.875rem 1rem 1rem;
}
[data-vibeui-block="card-012"] [data-part="title"]{
margin:0;font-size:1rem;font-weight:640;line-height:1.35;letter-spacing:-0.01em;
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
}
[data-vibeui-block="card-012"] [data-part="description"]{
margin:0;font-size:0.8125rem;line-height:1.5;color:var(--vibeui-card-012-muted);
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
}
[data-vibeui-block="card-012"] [data-part="meta"]{
display:flex;align-items:center;gap:0.375rem;margin-top:0.125rem;
font-size:0.75rem;color:var(--vibeui-card-012-muted);
}
`

/**
 * Карточка с кадром сверху и фиксированным соотношением сторон.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Card012({
  title = "Сборка каталога на container queries",
  description = "Разбираем, почему карточка обязана считать раскладку от своей ширины.",
  ratio = "16/9",
  badge = "Видео",
  duration = "12:40",
  meta = "Опубликовано вчера",
  media,
  accent,
  className,
  style,
  ...props
}: Card012Props) {
  const palette = {
    ...(accent ? { "--vibeui-card-012-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-card-012" precedence="medium">
        {STYLES}
      </style>
      <article
        {...props}
        data-vibeui-block="card-012"
        data-ratio={ratio}
        className={className}
        style={palette}
      >
        <div data-part="frame">
          {media}
          {badge ? <span data-part="badge">{badge}</span> : null}
          {duration ? <span data-part="duration">{duration}</span> : null}
        </div>
        <div data-part="body">
          <h3 data-part="title">{title}</h3>
          {description ? <p data-part="description">{description}</p> : null}
          {meta ? <p data-part="meta">{meta}</p> : null}
        </div>
      </article>
    </>
  )
}