Card

Adaptive Layout Card

A card that decides for itself whether to be a row or a tile: the frame sits on top in a narrow column and to the left in a wide one, measured against its own width.

  • card
  • container-query
  • responsive
  • layout

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-013?lang=en

Курс

Course: layout without media queries

Шесть занятий про container queries, :has() и подстройку компонента под место, в котором он оказался.

6 lessons · 3 hours

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-013" (Adaptive Layout 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-013.json

Registry item: https://vibeui.ru/r/card-013.json
Installs to: components/vibeui/card-013.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 that decides for itself whether to be a row or a tile: the frame sits on top in a narrow column and to the left in a wide one, measured against its own width.

One card in two layouts: vertical in a tight spot, horizontal in a wide one. The switch is an @container query on the card's own width. Zero dependencies, one file, no client JS.

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

<Card013
  eyebrow="Course"
  title="Layout without media queries"
  meta="6 lessons · 3 hours"
  actionLabel="Start"
/>

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-013-* palette — do not swap it for your theme tokens
- the layout on the inner shell rather than the root: @container applies to descendants only, and a rule on the container itself silently does nothing
- container-type:inline-size on the root — without it neither query ever runs
- @container instead of a media query: the same card lives in a sidebar and in the main column, and the window width knows nothing about it
- min-width:0 on the text column in row mode, or one long word stretches the flex line
- the 30rem threshold, tied to the point where the frame and three lines of text stop crowding each other

## 6. You may change
- the title, description and eyebrow copy and the meta caption
- the button text through actionLabel
- the accent through the accent prop — eyebrow, frame and button
- the @container thresholds for your grid: 30rem and 44rem are not sacred

## 7. Rules
- The container query measures the card, not the window: in a narrow sidebar it stays vertical on any monitor.
- The frame is empty: put an <img> inside [data-part="media"], the ratio and width are set.
- In row mode the frame stretches to the text height — a very long description makes it tall and thin.
- 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-013.json
https://vibeui.ru/r/card-013.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-card-013-*. Серверный: хуков нет. Корень объявлен container-type:inline-size, а вся раскладка лежит на внутреннем [data-part="shell"] — контейнерный запрос действует на потомков, но не на сам контейнер, и правило на корне молча ничего бы не изменило. Порогов два: на 30rem карточка становится горизонтальной, на 44rem кадр сужается, а описание ужимается до двух строк.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Card013Props = Omit<
  ComponentPropsWithoutRef<"article">,
  "children" | "title"
> & {
  title?: string
  description?: string
  /** Надпись над заголовком: раздел, автор, тип. */
  eyebrow?: string
  /** Подпись под текстом: срок, цена, статус. */
  meta?: string
  actionLabel?: string
  accent?: string
}

// Идея компонента: одна карточка на две раскладки. В узкой колонке кадр
// стоит сверху, в широкой — уезжает влево, и карточка становится строкой
// списка. Переключает это @container, а не медиазапрос: карточка живёт
// то в сайдбаре, то в основной колонке, и ширина окна о ней ничего не знает.
const STYLES = `
:where([data-vibeui-block="card-013"]){
--vibeui-card-013-bg:oklch(1 0 0);
--vibeui-card-013-fg:oklch(0.22 0.015 265);
--vibeui-card-013-muted:oklch(0.55 0.013 265);
--vibeui-card-013-border:oklch(0.91 0.006 265);
--vibeui-card-013-accent:oklch(0.55 0.15 195);
--vibeui-card-013-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
container-type:inline-size;
}
[data-vibeui-block="card-013"]{
display:block;width:100%;max-width:40rem;box-sizing:border-box;
background:var(--vibeui-card-013-bg);color:var(--vibeui-card-013-fg);
border:1px solid var(--vibeui-card-013-border);border-radius:0.9375rem;
font-family:var(--vibeui-card-013-font);overflow:hidden;
}
/* Раскладка живёт на shell, а не на корне: контейнерный запрос не действует
   на сам контейнер, и правило на корне молча ничего бы не изменило. */
[data-vibeui-block="card-013"] [data-part="shell"]{
display:flex;flex-direction:column;
}
[data-vibeui-block="card-013"] [data-part="media"]{
flex:none;aspect-ratio:16 / 9;
background:
radial-gradient(100% 90% at 25% 10%,color-mix(in oklab,var(--vibeui-card-013-accent) 45%,transparent),transparent 65%),
linear-gradient(150deg,oklch(0.94 0.03 200),oklch(0.87 0.05 220));
}
[data-vibeui-block="card-013"] [data-part="body"]{
display:flex;flex-direction:column;gap:0.375rem;padding:0.9375rem 1.0625rem 1.0625rem;
}
[data-vibeui-block="card-013"] [data-part="eyebrow"]{
font-size:0.6875rem;font-weight:650;letter-spacing:0.07em;text-transform:uppercase;
color:var(--vibeui-card-013-accent);
}
[data-vibeui-block="card-013"] [data-part="title"]{
margin:0;font-size:1.0625rem;font-weight:640;line-height:1.3;letter-spacing:-0.01em;
}
[data-vibeui-block="card-013"] [data-part="description"]{
margin:0;font-size:0.875rem;line-height:1.5;color:var(--vibeui-card-013-muted);
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;overflow:hidden;
}
[data-vibeui-block="card-013"] [data-part="foot"]{
display:flex;align-items:center;justify-content:space-between;gap:0.75rem;
margin-top:auto;padding-top:0.625rem;
}
[data-vibeui-block="card-013"] [data-part="meta"]{
margin:0;font-size:0.75rem;color:var(--vibeui-card-013-muted);
}
[data-vibeui-block="card-013"] [data-part="action"]{
appearance:none;cursor:pointer;flex:none;
height:2rem;padding:0 0.875rem;border-radius:0.5rem;
border:1px solid color-mix(in oklab,var(--vibeui-card-013-accent) 35%,var(--vibeui-card-013-border));
background:color-mix(in oklab,var(--vibeui-card-013-accent) 10%,oklch(1 0 0));
color:color-mix(in oklab,var(--vibeui-card-013-accent) 75%,oklch(0.2 0.02 265));
font:inherit;font-size:0.8125rem;font-weight:640;
transition:background-color .16s ease;
}
[data-vibeui-block="card-013"] [data-part="action"]:hover{
background:color-mix(in oklab,var(--vibeui-card-013-accent) 18%,oklch(1 0 0));
}
[data-vibeui-block="card-013"] [data-part="action"]:focus-visible{
outline:2px solid var(--vibeui-card-013-accent);outline-offset:2px;
}
/* Своя ширина, а не ширина окна: карточка одинаково ведёт себя в сайдбаре
   и в основной колонке. */
@container (min-width: 30rem){
[data-vibeui-block="card-013"] [data-part="shell"]{flex-direction:row;align-items:stretch}
[data-vibeui-block="card-013"] [data-part="media"]{width:38%;max-width:13rem;aspect-ratio:auto}
[data-vibeui-block="card-013"] [data-part="body"]{flex:1;min-width:0;padding:1.0625rem 1.125rem}
[data-vibeui-block="card-013"] [data-part="title"]{font-size:1.125rem}
}
@container (min-width: 44rem){
[data-vibeui-block="card-013"] [data-part="media"]{width:32%;max-width:15rem}
[data-vibeui-block="card-013"] [data-part="description"]{-webkit-line-clamp:2}
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="card-013"] *{animation:none!important;transition:none!important}}
`

/**
 * Карточка, которая переключается между горизонтальной и вертикальной
 * раскладкой по собственной ширине. Один файл, ноль зависимостей.
 */
export function Card013({
  title = "Курс: раскладка без медиазапросов",
  description = "Шесть занятий про container queries, :has() и подстройку компонента под место, в котором он оказался.",
  eyebrow = "Курс",
  meta = "6 занятий · 3 часа",
  actionLabel = "Начать",
  accent,
  className,
  style,
  ...props
}: Card013Props) {
  const palette = {
    ...(accent ? { "--vibeui-card-013-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-card-013" precedence="medium">
        {STYLES}
      </style>
      <article
        {...props}
        data-vibeui-block="card-013"
        className={className}
        style={palette}
      >
        <div data-part="shell">
          <div data-part="media" aria-hidden="true" />
          <div data-part="body">
            {eyebrow ? <span data-part="eyebrow">{eyebrow}</span> : null}
            <h3 data-part="title">{title}</h3>
            {description ? <p data-part="description">{description}</p> : null}
            <div data-part="foot">
              {meta ? <p data-part="meta">{meta}</p> : null}
              <button data-part="action" type="button">
                {actionLabel}
              </button>
            </div>
          </div>
        </div>
      </article>
    </>
  )
}