Avatar

Person Row

A person row that is clickable end to end: the link is stretched by a pseudo-element and the action button is raised above it as a separate target.

  • avatar
  • row
  • list
  • profile

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/avatar-017?lang=en

Ким СонAnalystбыл в сети 20 минут назад
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 "avatar-017" (Person Row) 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/avatar-017.json

Registry item: https://vibeui.ru/r/avatar-017.json
Installs to: components/vibeui/avatar-017.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 person row that is clickable end to end: the link is stretched by a pseudo-element and the action button is raised above it as a separate target.

A person row: the link stretches across the whole row, the action button stays a separate target, and focus highlights the entire row. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Avatar017 } from "@/components/vibeui/avatar-017"

<Avatar017
  name="Kim Son"
  role="Analyst"
  meta="last seen 20 minutes ago"
  action="Message"
  href="/people/kim"
/>

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-avatar-017-* palette — do not swap it for your theme tokens
- the stretched link via a::after: the tap target must be the row, not the word
- the action button raised above the link by z-index — otherwise it stops being clickable
- the focus ring on the row through :has(): an underlined name misstates the real target
- the different weights on role and meta lines: grey on grey merges into one
- the row's own light surface: without it the dark text disappears on a dark background

## 6. You may change
- the name, the role through role and the meta line
- the button caption through action
- the link address through href
- the row width through max-width

## 7. Rules
- A stretched link breaks text selection inside the row: that is the price of a large tap target.
- There is one action button: a second one has to sit beside it and be checked on phones.
- The button's accessible label names the person — in a list of ten rows "Message" alone is useless.
- 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/avatar-017.json
https://vibeui.ru/r/avatar-017.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-avatar-017-*. Серверный: хуков нет. Вся строка — цель ссылки: a::after растянут на inset:0, а кнопка справа поднята z-index, поэтому открыть профиль и написать сообщение остаются разными действиями. Обводка фокуса ставится на строку через :has(a:focus-visible), поэтому с клавиатуры видно всю цель, а не подчёркнутое имя. Роль и служебная строка разведены начертанием: два одинаковых серых текста сливаются в один.

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 Avatar017Props = Omit<ComponentPropsWithoutRef<"a">, "children"> & {
  name?: string
  role?: string
  meta?: string
  action?: string
}

// Идея компонента: строка человека, кликабельная целиком. Ссылка растянута
// псевдоэлементом, а кнопка справа поднята над ней z-index: открыть профиль и
// написать сообщение — разные действия, и объединять их в одну цель нельзя.
// Обводка фокуса ставится на строку через :has(), поэтому с клавиатуры видно
// всю цель, а не подчёркнутое имя. Роль и служебная строка разведены по
// начертанию: две одинаковые серые строки сливаются в одну.
const STYLES = `
:where([data-vibeui-block="avatar-017"]){
--vibeui-avatar-017-size:2.5rem;
--vibeui-avatar-017-bg:oklch(1 0 0);
--vibeui-avatar-017-fg:oklch(0.24 0.014 265);
--vibeui-avatar-017-muted:oklch(0.55 0.014 265);
--vibeui-avatar-017-border:oklch(0.91 0.006 265);
--vibeui-avatar-017-hover:oklch(0.55 0.02 265 / 5%);
--vibeui-avatar-017-accent:oklch(0.55 0.2 262);
--vibeui-avatar-017-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Строка целиком — цель ссылки, кнопка поднята над ней z-index. */
[data-vibeui-block="avatar-017"]{
position:relative;display:flex;align-items:center;gap:0.625rem;
box-sizing:border-box;width:100%;max-width:24rem;padding:0.5rem 0.625rem;
background:var(--vibeui-avatar-017-bg);
border:1px solid var(--vibeui-avatar-017-border);border-radius:0.75rem;
font-family:var(--vibeui-avatar-017-font);color:var(--vibeui-avatar-017-fg);
}
[data-vibeui-block="avatar-017"] *{box-sizing:border-box}
[data-vibeui-block="avatar-017"]:hover{background:var(--vibeui-avatar-017-hover)}
[data-vibeui-block="avatar-017"]:has(a:focus-visible){outline:2px solid var(--vibeui-avatar-017-accent);outline-offset:2px}
[data-vibeui-block="avatar-017"] [data-part="face"]{
display:grid;place-items:center;flex:none;
width:var(--vibeui-avatar-017-size);height:var(--vibeui-avatar-017-size);
border-radius:9999px;
background:oklch(0.9 0.06 var(--vibeui-avatar-017-hue,265));
color:oklch(0.36 0.12 var(--vibeui-avatar-017-hue,265));
font-size:calc(var(--vibeui-avatar-017-size) * 0.34);font-weight:700;
}
[data-vibeui-block="avatar-017"] [data-part="text"]{display:flex;flex-direction:column;gap:0.0625rem;min-width:0;flex:1 1 auto}
[data-vibeui-block="avatar-017"] [data-part="name"]{
margin:0;font-size:0.875rem;font-weight:650;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="avatar-017"] [data-part="name"] a{color:inherit;text-decoration:none}
[data-vibeui-block="avatar-017"] [data-part="name"] a::after{content:"";position:absolute;inset:0}
/* Роль и служебная строка разведены начертанием: серое на сером сливается. */
[data-vibeui-block="avatar-017"] [data-part="role"]{
margin:0;font-size:0.75rem;font-weight:600;color:var(--vibeui-avatar-017-fg);
}
[data-vibeui-block="avatar-017"] [data-part="meta"]{
margin:0;font-size:0.6875rem;color:var(--vibeui-avatar-017-muted);
}
[data-vibeui-block="avatar-017"] [data-part="action"]{
position:relative;z-index:1;flex:none;
appearance:none;cursor:pointer;height:1.875rem;padding:0 0.625rem;
border:1px solid var(--vibeui-avatar-017-border);border-radius:0.5rem;
background:var(--vibeui-avatar-017-bg);color:inherit;
font:inherit;font-size:0.75rem;font-weight:650;
}
[data-vibeui-block="avatar-017"] [data-part="action"]:focus-visible{outline:2px solid var(--vibeui-avatar-017-accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="avatar-017"] *{animation:none!important;transition:none!important}}
`

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
}

function initials(name: string) {
  return name
    .split(" ")
    .slice(0, 2)
    .map((part) => part.charAt(0).toUpperCase())
    .join("")
}

/**
 * Строка человека: ссылка на всю строку, кнопка действия — отдельная цель.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Avatar017({
  name = "Ким Сон",
  role = "Аналитик",
  meta = "был в сети 20 минут назад",
  action = "Написать",
  href = "#",
  className,
  style,
  ...props
}: Avatar017Props) {
  const palette = {
    "--vibeui-avatar-017-hue": hue(name),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-avatar-017" precedence="medium">
        {STYLES}
      </style>
      <div data-vibeui-block="avatar-017" className={className} style={palette}>
        <span data-part="face" aria-hidden="true">
          {initials(name)}
        </span>
        <span data-part="text">
          <span data-part="name">
            <a {...props} href={href}>
              {name}
            </a>
          </span>
          <span data-part="role">{role}</span>
          <span data-part="meta">{meta}</span>
        </span>
        <button
          type="button"
          data-part="action"
          aria-label={`${action}: ${name}`}
        >
          {action}
        </button>
      </div>
    </>
  )
}