Motiva
Мои коллекции
← каталог

Masonry Columns

Пинтерест-стена на чистом CSS: одно свойство columns, ни строчки JS и ни одного медиазапроса. Честно описано ограничение — порядок чтения идёт по колонкам.

csshoverrevealbasic~2 КБreduced-motion ✓
↕ демо длиннее кадра — прокрутите его

Пропсы

Разобрано из кода записи: обязательные — без «?», у остальных показано значение по умолчанию.

ПропТипПо умолчаниюЧто это
items?MasonryColumnsItem[]ITEMS
min?number210Минимальная ширина колонки, px (формат «210»).
gap?number12Зазор, px (формат «12»).
MasonryColumnsItem — что внутри
  • src: string
  • title?: string
  • tag?: string

Установка

npx shadcn@latest add https://motiva.pages.dev/r/masonry-columns.json
Что нужно в проекте до установки
  1. components.json в корне — с алиасами в форме"components": "@/components". Нет файла — CLI не поймёт, куда класть.
  2. Алиас @ объявлен дважды: "paths": {"@/*": ["./src/*"]} в корневом tsconfig.json (в Vite он часто содержит только references — тогда CLI создаёт папку@) и resolve.alias в конфиге сборщика.
  3. Токены подключены один раз на приложение:import "@/styles/motiva-tokens.css" — они приезжают вместе с записью. Без них секция потеряет цвета, ритм и тему.
  4. Своя типографика не должна спорить: если у вас на обёрткеtext-align: center, он протечёт внутрь блоков.
  5. Проект без TypeScript? Всё равно нужен минимальныйtsconfig.json с pathsи "tsx": true в components.json: CLI грузит tsconfig безусловно, а с "tsx": false идёт за jsconfig.json и падает так же. Приехавший рядом.tsx можно не трогать — вам нужен.vanilla.js.

Экспорт

↓ Скачать .zip

Код

export interface MasonryColumnsItem {
  src: string;
  title?: string;
  tag?: string;
}

export interface MasonryColumnsProps {
  items?: MasonryColumnsItem[];
  /** Минимальная ширина колонки, px (формат «210»). */
  min?: number;
  /** Зазор, px (формат «12»). */
  gap?: number;
}

const p = (id: number, w: number, h: number) => `https://motiva.pages.dev/img/${id}-${w}x${h}.jpg`;

const ITEMS: MasonryColumnsItem[] = [
  { src: p(1015, 600, 800), title: "Fjord", tag: "пейзаж" },
  { src: p(1018, 600, 400), title: "Ridge", tag: "горы" },
  { src: p(1024, 600, 900), title: "Bear", tag: "дикая природа" },
  { src: p(1035, 600, 400), title: "Coast", tag: "море" },
  { src: p(1039, 600, 750), title: "Falls", tag: "вода" },
  { src: p(1043, 600, 400), title: "Valley", tag: "лес" },
  { src: p(1044, 600, 850), title: "Pines", tag: "лес" },
  { src: p(1050, 600, 400), title: "Skyline", tag: "город" },
  { src: p(1056, 600, 800), title: "Dunes", tag: "пустыня" },
  { src: p(1069, 600, 450), title: "Rooftop", tag: "город" },
  { src: p(1074, 600, 900), title: "Shore", tag: "берег" },
  { src: p(1080, 600, 500), title: "Berries", tag: "макро" },
];

/**
 * Пинтерест-стена на чистом CSS: ни строчки JS, ни одного замера.
 *
 * Вся раскладка — свойство `columns`. Указываешь ЖЕЛАЕМУЮ ширину колонки, а не их число:
 * браузер сам решает, сколько поместится, и растягивает остаток. Медиазапросы не нужны
 * в принципе — это самый дешёвый masonry, который вообще бывает.
 *
 * ЧЕСТНОЕ ОГРАНИЧЕНИЕ, из-за которого этот вариант годится не всегда: порядок чтения
 * идёт ПО КОЛОНКАМ — сверху вниз в первой, потом сверху вниз во второй. Для стены фотографий
 * это незаметно, но для ленты новостей или списка товаров порядок будет неправильным
 * и для глаза, и для скринридера. Нужен порядок слева направо — бери `masonry-grid`,
 * там раскладка считается вручную именно ради этого.
 *
 * Второе ограничение: разрывы. `break-inside: avoid` не даёт резать плитку между колонками,
 * но высоту колонок браузер выравнивает как умеет — идеально ровного низа не будет.
 */
export function MasonryColumns({ items = ITEMS, min = 210, gap = 12 }: MasonryColumnsProps) {
  return (
    <div
      className="msc"
      style={{ ["--msc-min" as string]: `${min}px`, ["--msc-gap" as string]: `${gap}px` }}
    >
      <style>{`
        /* вся раскладка — эта одна строка */
        .msc { columns: var(--msc-min, 210px); column-gap: var(--msc-gap, 12px); width: 100%; }

        .msc-item { break-inside: avoid; margin: 0 0 var(--msc-gap, 12px); position: relative;
          border-radius: var(--ds-radius, 14px); overflow: hidden;
          background: var(--ds-surface, oklch(0.18 0.02 275)); }
        .msc-item img { display: block; width: 100%; height: auto;
          transition: transform var(--ds-slow, 0.6s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)); }
        /* приближается картинка внутри неподвижной рамки — колонки не пересчитываются */
        .msc-item:hover img { transform: scale(1.06); }

        .msc-meta { position: absolute; inset: auto 0 0 0; padding: 30px 12px 10px;
          color: oklch(0.98 0 0);
          background: linear-gradient(to top, oklch(0 0 0 / 0.76), oklch(0 0 0 / 0));
          opacity: 0; translate: 0 20%;
          transition: opacity var(--ds-base, 0.32s),
                      translate var(--ds-base, 0.32s) var(--ds-ease-out, cubic-bezier(0.16, 1, 0.3, 1)); }
        .msc-item:hover .msc-meta, .msc-item:focus-within .msc-meta { opacity: 1; translate: 0 0; }
        .msc-t { display: block; font-weight: 700; font-size: var(--ds-text-sm, 0.9rem);
          letter-spacing: var(--ds-tracking-tight, -0.03em);
          font-family: var(--ds-font, system-ui, sans-serif); }
        .msc-g { font-family: var(--ds-font-mono, ui-monospace, monospace);
          font-size: var(--ds-text-xs, 0.72rem); opacity: 0.8; }

        @media (prefers-reduced-motion: reduce) {
          .msc-item img, .msc-meta { transition: none; }
          .msc-meta { opacity: 1; translate: 0 0; }
        }
      `}</style>

      {items.map((it) => (
        <figure key={it.src} className="msc-item">
          <img src={it.src} alt={it.title ?? ""} loading="lazy" decoding="async" />
          {it.title && (
            <figcaption className="msc-meta">
              <span className="msc-t">{it.title}</span>
              {it.tag && <span className="msc-g">{it.tag}</span>}
            </figcaption>
          )}
        </figure>
      ))}
    </div>
  );
}
лицензия: MITавтор: Motivaисточник: оригинал (Motiva)v1.0.0