component

Attachment

A layout-only wrapper that pins an arbitrary content element to the outer bounds of a target element. It’s content-agnostic — drop in badges, stamps, status dots, icons, or anything else. Positioning is handled by Floating UI with collision-aware flip and shift, and auto-updates on scroll and resize.

The wrapper hugs its target (display: inline-flex, position: relative) and the content slot is absolutely positioned against it. By default the content’s center sits on the target’s corner (badge convention) — switch to anchor="outside" for flush-outside placement.

Default

A status dot pinned to the bottom-end corner of an avatar-like element.

---
import Attachment from "../Attachment.astro";
---

<Attachment placement="bottom-end" shape="circle">
  <div
    style="width: 64px; height: 64px; background: #d0d7de; border-radius: 50%;"
  >
  </div>
  <span
    slot="content"
    style="width: 14px; height: 14px; background: #2ecc71; border: 2px solid white; border-radius: 50%; display: block;"
  ></span>
</Attachment>

Placements

The placement prop accepts any Floating UI Placementtop-start, top-end, bottom-start, bottom-end, left, right, and so on.

3
top-start
3
top-end
3
bottom-start
3
bottom-end
---
import Attachment from "../Attachment.astro";
import type { Placement } from "@pindoba/core-attachment";

const placements: Placement[] = [
  "top-start",
  "top-end",
  "bottom-start",
  "bottom-end",
];
---

<div style="display: flex; gap: 2rem; flex-wrap: wrap;">
  {
    placements.map((placement) => (
      <div style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;">
        <Attachment placement={placement}>
          <div style="width: 56px; height: 56px; background: #d0d7de; border-radius: 8px;" />
          <span
            slot="content"
            style="min-width: 18px; height: 18px; padding: 0 6px; background: #e74c3c; color: white; border-radius: 999px; font-size: 11px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center;"
          >
            3
          </span>
        </Attachment>
        <code style="font-size: 11px;">{placement}</code>
      </div>
    ))
  }
</div>

Anchor

Switch anchor to "outside" when you want the attachment to sit flush outside the target — common for tooltips, pointer labels, or callouts.

9
corner
9
outside
---
import Attachment from "../Attachment.astro";
---

<div style="display: flex; gap: 3rem;">
  <div
    style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"
  >
    <Attachment placement="top-end" anchor="corner">
      <div
        style="width: 64px; height: 64px; background: #d0d7de; border-radius: 8px;"
      >
      </div>
      <span
        slot="content"
        style="min-width: 20px; height: 20px; padding: 0 6px; background: #e74c3c; color: white; border-radius: 999px; font-size: 12px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center;"
      >
        9
      </span>
    </Attachment>
    <code style="font-size: 11px;">corner</code>
  </div>

  <div
    style="display: flex; flex-direction: column; align-items: center; gap: 0.5rem;"
  >
    <Attachment placement="top-end" anchor="outside">
      <div
        style="width: 64px; height: 64px; background: #d0d7de; border-radius: 8px;"
      >
      </div>
      <span
        slot="content"
        style="min-width: 20px; height: 20px; padding: 0 6px; background: #e74c3c; color: white; border-radius: 999px; font-size: 12px; font-weight: 600; display: inline-flex; align-items: center; justify-content: center;"
      >
        9
      </span>
    </Attachment>
    <code style="font-size: 11px;">outside</code>
  </div>
</div>

Offset

offset stacks on top of the anchor — positive pushes the content further from the target, negative pulls it closer in.

Card title

Body text.

NEW
---
import Attachment from "../Attachment.astro";
---

<Attachment placement="top-start" offset={-8}>
  <div
    style="width: 200px; height: 120px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 12px; padding: 1rem;"
  >
    <strong>Card title</strong>
    <p style="margin: 0.5rem 0 0; color: #57606a; font-size: 14px;">
      Body text.
    </p>
  </div>
  <span
    slot="content"
    style="padding: 2px 8px; background: #0969da; color: white; border-radius: 4px; font-size: 11px; font-weight: 700; letter-spacing: 0.05em;"
  >
    NEW
  </span>
</Attachment>

Badges

Numeric counters, unread dots, and presence indicators — the classic “notification badge” use case. Default anchor="corner" keeps the badge centered on the target’s corner.

3
99+
New
Online
---
import Attachment from "../Attachment.astro";
import Badge from "@pindoba/astro-badge";
---

<div style="display: flex; gap: 2.5rem; align-items: center;">
  <Attachment placement="top-end">
    <div
      style="width: 48px; height: 48px; background: #d0d7de; border-radius: 8px;"
    >
    </div>
    <Badge slot="content" size="sm" feedback="danger">3</Badge>
  </Attachment>

  <Attachment placement="top-end">
    <div
      style="width: 48px; height: 48px; background: #d0d7de; border-radius: 8px;"
    >
    </div>
    <Badge slot="content" size="sm" feedback="danger">99+</Badge>
  </Attachment>

  <Attachment placement="top-end" shape="circle">
    <div
      style="width: 48px; height: 48px; background: #d0d7de; border-radius: 50%;"
    >
    </div>
    <Badge slot="content" size="sm" feedback="danger">New</Badge>
  </Attachment>

  <Attachment placement="bottom-end" shape="circle">
    <div
      style="width: 48px; height: 48px; background: #d0d7de; border-radius: 50%;"
    >
    </div>
    <Badge slot="content" size="sm" feedback="success">Online</Badge>
  </Attachment>
</div>

Stamps

Marketing stamps like NEW, -30%, or a rotated SOLD overlay. A negative offset pulls the stamp inward so it overlaps the card edge.

Wireless Headphones

Premium audio, noise cancelling.

Leather Wallet

Handcrafted, full-grain leather.

Vintage Camera

Rare 1970s film shooter.

---
import Attachment from "../Attachment.astro";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
---

<div style="display: flex; gap: 2.5rem; flex-wrap: wrap;">
  <Attachment placement="top-start" offset={-4}>
    <div
      style="width: 200px; height: 120px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 12px; padding: 1rem;"
    >
      <strong>Wireless Headphones</strong>
      <p style="margin: 0.5rem 0 0; color: #57606a; font-size: 13px;">
        Premium audio, noise cancelling.
      </p>
    </div>
    <Stamp slot="content" size="xs" emphasis="primary"
      ><Icon name="lucide:star" /></Stamp
    >
  </Attachment>

  <Attachment placement="top-end" offset={-8}>
    <div
      style="width: 200px; height: 120px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 12px; padding: 1rem;"
    >
      <strong>Leather Wallet</strong>
      <p style="margin: 0.5rem 0 0; color: #57606a; font-size: 13px;">
        Handcrafted, full-grain leather.
      </p>
    </div>
    <Stamp slot="content" emphasis="secondary"
      ><Icon name="lucide:flame" /></Stamp
    >
  </Attachment>

  <Attachment placement="top-end" offset={-8}>
    <div
      style="width: 200px; height: 120px; background: #f6f8fa; border: 1px solid #d0d7de; border-radius: 12px; padding: 1rem;"
    >
      <strong>Vintage Camera</strong>
      <p style="margin: 0.5rem 0 0; color: #57606a; font-size: 13px;">
        Rare 1970s film shooter.
      </p>
    </div>
    <Stamp slot="content" emphasis="muted"><Icon name="lucide:check" /></Stamp>
  </Attachment>
</div>

Props

props · 8 total
prop type default req description
placement Placement "top-end" Floating UI placement relative to the target's outer bounds.
anchor "corner""outside" "corner" "corner" centers the attachment on the target corner (badges, dots). "outside" places it flush against the target edge (tooltips, popovers).
shape "rect""circle" "rect" Silhouette of the target. Use "circle" so diagonal placements hug the arc of a round target instead of floating off its bounding corner.
offset number 0 Extra px nudge along the placement axis, stacked on top of the anchor.
flip boolean true Flip to the opposite side when overflowing the viewport.
shift boolean true Shift along the alignment axis to stay within the viewport.
padding number 0 Viewport padding used by the flip/shift middleware.
passThrough { root?, content? } Override slot styles or props.