Card

Bookmark Card

A bookmark card for an outside link: the domain is visible before the click, the whole surface is the target, and the markup still holds one link.

  • card
  • link
  • bookmark
  • external

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-011?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 "card-011" (Bookmark 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-011.json

Registry item: https://vibeui.ru/r/card-011.json
Installs to: components/vibeui/card-011.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 bookmark card for an outside link: the domain is visible before the click, the whole surface is the target, and the markup still holds one link.

A bookmark card for an outside link: a letter mark for the source, the domain in plain text, a title link covering the whole surface and a two-line description. Zero dependencies, one file, no client JS.

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

<Card011
  title="What container queries are for"
  description="A walk-through with examples."
  url="web.dev"
  href="https://web.dev"
/>

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-011-* palette — do not swap it for your theme tokens (bg-card, text-muted-foreground and the like)
- the domain as visible text, not just a letter mark: before leaving the site a reader must see where they are going
- the single-link technique: position:relative on the card and ::after on the title — three links per card turn the list to mush in a screen reader
- the visually hidden "opens in a new tab": an unannounced tab switch disorients
- the focus ring on the whole card via :has(a:focus-visible) rather than an underline on the title alone
- the domain-derived hue of the mark: one source keeps one colour and gets recognised in a list

## 6. You may change
- the title and description copy
- the domain through url — it also colours the letter mark
- the destination through href and the external flag
- the accent through the accent prop — the arrow and the hover border

## 7. Rules
- A stretched link breaks text selection inside the card: that is the price of a large tap target.
- A second link or button ends up under the invisible ::after layer — lift it with position:relative and z-index.
- url is a caption, not the source of href: the component parses nothing out of the address.
- 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-011.json
https://vibeui.ru/r/card-011.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-card-011-*. Серверный: хуков нет. Ссылка заголовка растянута псевдоэлементом ::after на всю карточку, поэтому цель нажатия — вся площадь, а в списке ссылок остаётся одна запись на карточку. Обводку фокуса карточка берёт на себя через :has(a:focus-visible). Оттенок буквенного знака выводится из домена по FNV-1a, поэтому один источник всегда одного цвета. Внешняя ссылка получает target и rel, а предупреждение о новой вкладке уезжает в визуально скрытый текст.

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 Card011Props = Omit<
  ComponentPropsWithoutRef<"article">,
  "children" | "title"
> & {
  title?: string
  description?: string
  /** Домен источника: он же рисует буквенный знак и оттенок. */
  url?: string
  href?: string
  /** Внешняя ссылка получает target и rel, а стрелка — поворот. */
  external?: boolean
  accent?: string
}

// Идея компонента: закладка на внешний материал. Кликается вся площадь, но
// ссылка в дереве доступности одна — заголовок растягивает свою зону через
// ::after. Домен показан текстом, а не только знаком: перед уходом с сайта
// человек должен видеть, куда именно он уходит.
const STYLES = `
:where([data-vibeui-block="card-011"]){
--vibeui-card-011-bg:oklch(1 0 0);
--vibeui-card-011-fg:oklch(0.23 0.015 265);
--vibeui-card-011-muted:oklch(0.55 0.013 265);
--vibeui-card-011-border:oklch(0.91 0.006 265);
--vibeui-card-011-accent:oklch(0.55 0.18 262);
--vibeui-card-011-hue:250;
--vibeui-card-011-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="card-011"]{
position:relative;display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:24rem;box-sizing:border-box;padding:0.9375rem 1rem;
background:var(--vibeui-card-011-bg);color:var(--vibeui-card-011-fg);
border:1px solid var(--vibeui-card-011-border);border-radius:0.875rem;
font-family:var(--vibeui-card-011-font);
transition:border-color .16s ease,box-shadow .16s ease;
}
[data-vibeui-block="card-011"]:hover{
border-color:color-mix(in oklab,var(--vibeui-card-011-accent) 40%,var(--vibeui-card-011-border));
box-shadow:0 10px 24px -18px oklch(0.2 0.03 265 / 55%);
}
[data-vibeui-block="card-011"]:has(a:focus-visible){
border-color:var(--vibeui-card-011-accent);
box-shadow:0 0 0 3px color-mix(in oklab,var(--vibeui-card-011-accent) 22%,transparent);
}
[data-vibeui-block="card-011"] [data-part="source"]{
display:flex;align-items:center;gap:0.5rem;
font-size:0.75rem;color:var(--vibeui-card-011-muted);
}
[data-vibeui-block="card-011"] [data-part="mark"]{
display:flex;align-items:center;justify-content:center;flex:none;
width:1.375rem;height:1.375rem;border-radius:0.4375rem;
background:oklch(0.93 0.05 var(--vibeui-card-011-hue));
color:oklch(0.4 0.1 var(--vibeui-card-011-hue));
font-size:0.6875rem;font-weight:700;
}
[data-vibeui-block="card-011"] [data-part="host"]{
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="card-011"] [data-part="arrow"]{
margin-inline-start:auto;flex:none;
width:0.9375rem;height:0.9375rem;
color:var(--vibeui-card-011-muted);
transition:transform .16s ease,color .16s ease;
}
[data-vibeui-block="card-011"]:hover [data-part="arrow"]{
transform:translate(2px,-2px);color:var(--vibeui-card-011-accent);
}
[data-vibeui-block="card-011"] [data-part="title"]{
margin:0;font-size:1rem;font-weight:640;line-height:1.35;letter-spacing:-0.01em;
}
[data-vibeui-block="card-011"] [data-part="title"] a{color:inherit;text-decoration:none;outline:none}
/* Заголовок-ссылка накрывает карточку целиком: одна ссылка, вся площадь. */
[data-vibeui-block="card-011"] [data-part="title"] a::after{
content:"";position:absolute;inset:0;border-radius:inherit;
}
[data-vibeui-block="card-011"] [data-part="sr"]{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;
overflow:hidden;clip-path:inset(50%);white-space:nowrap;
}
[data-vibeui-block="card-011"] [data-part="description"]{
margin:0;font-size:0.8125rem;line-height:1.5;color:var(--vibeui-card-011-muted);
display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="card-011"] *{animation:none!important;transition:none!important}}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="card-011"]:hover [data-part="arrow"]{transform:none}}
`

function hue(name: string) {
  let hash = 2166136261

  for (const symbol of name) {
    hash ^= symbol.codePointAt(0)!
    hash = Math.imul(hash, 16777619)
  }

  return ((hash >>> 0) % 12) * 30
}

/**
 * Карточка-закладка на внешний материал: домен, заголовок-ссылка на всю
 * площадь и описание. Один файл, ноль зависимостей, собственная палитра.
 */
export function Card011({
  title = "Что такое container queries и когда они нужны",
  description = "Разбор на примерах: почему раскладку компонента считают от его собственной ширины, а не от окна.",
  url = "web.dev",
  href = "https://web.dev",
  external = true,
  accent,
  className,
  style,
  ...props
}: Card011Props) {
  const palette = {
    "--vibeui-card-011-hue": hue(url),
    ...(accent ? { "--vibeui-card-011-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-card-011" precedence="medium">
        {STYLES}
      </style>
      <article
        {...props}
        data-vibeui-block="card-011"
        className={className}
        style={palette}
      >
        <p data-part="source">
          <span data-part="mark" aria-hidden="true">
            {url
              .replace(/^www\./, "")
              .charAt(0)
              .toUpperCase()}
          </span>
          <span data-part="host">{url}</span>
          <svg
            data-part="arrow"
            viewBox="0 0 16 16"
            fill="none"
            aria-hidden="true"
          >
            <path
              d="M4.5 11.5 11.5 4.5M6 4.5h5.5V10"
              stroke="currentColor"
              strokeWidth="1.6"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
        </p>
        <h3 data-part="title">
          <a
            href={href}
            {...(external
              ? { target: "_blank", rel: "noopener noreferrer" }
              : null)}
          >
            {title}
            {external ? (
              <span data-part="sr"> (откроется в новой вкладке)</span>
            ) : null}
          </a>
        </h3>
        {description ? <p data-part="description">{description}</p> : null}
      </article>
    </>
  )
}