component
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 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, or anchor="inside" to tuck it fully within the target’s bounds.
A status dot pinned to the bottom-end corner of an avatar-like element.
---
import Attachment from "../Attachment.astro";
import { css } from "@pindoba/styled-system/css";
const avatar = css({
width: "64px",
height: "64px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "full",
});
const status = css({
width: "14px",
height: "14px",
background: "success",
border: "2px solid",
borderColor: "neutral.surface.hill",
borderRadius: "full",
display: "block",
});
---
<Attachment placement="bottom-end" shape="circle">
<div class={avatar}></div>
<span slot="content" class={status}></span>
</Attachment>The placement prop accepts any Floating UI Placement — top-start, top-end, bottom-start, bottom-end, left, right, and so on.
top-starttop-endbottom-startbottom-end---
import Attachment from "../Attachment.astro";
import type { Placement } from "@pindoba/core-attachment";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const placements: Placement[] = [
"top-start",
"top-end",
"bottom-start",
"bottom-end",
];
const row = flex({ gap: "xl", wrap: "wrap" });
const cell = flex({ direction: "column", align: "center", gap: "xs" });
const thumb = css({
width: "56px",
height: "56px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "md",
});
const dot = css({
colorPalette: "primary",
minWidth: "18px",
height: "18px",
paddingX: "xs",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
borderRadius: "full",
fontSize: "xs",
fontWeight: "semibold",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
const label = css({ fontSize: "xs" });
---
<div class={row}>
{
placements.map((placement) => (
<div class={cell}>
<Attachment placement={placement}>
<div class={thumb} />
<span slot="content" class={dot}>
3
</span>
</Attachment>
<code class={label}>{placement}</code>
</div>
))
}
</div>Switch anchor to "outside" when you want the attachment to sit flush outside the target — common for tooltips, pointer labels, or callouts. Use "inside" when nothing may overhang: the attachment sits fully within the target’s bounds, flush against the placement corner, and offset becomes an inward inset. That’s the right pick for dense lists and narrow rails, where a corner-anchored badge would land on the neighbouring row or on a scrollbar.
corneroutsideinside---
import Attachment from "../Attachment.astro";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const row = flex({ gap: "3xl" });
const cell = flex({ direction: "column", align: "center", gap: "xs" });
const thumb = css({
width: "64px",
height: "64px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "md",
});
const dot = css({
colorPalette: "primary",
minWidth: "20px",
height: "20px",
paddingX: "xs",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
borderRadius: "full",
fontSize: "xs",
fontWeight: "semibold",
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
});
const label = css({ fontSize: "xs" });
---
<div class={row}>
<div class={cell}>
<Attachment placement="top-end" anchor="corner">
<div class={thumb}></div>
<span slot="content" class={dot}>9</span>
</Attachment>
<code class={label}>corner</code>
</div>
<div class={cell}>
<Attachment placement="top-end" anchor="outside">
<div class={thumb}></div>
<span slot="content" class={dot}>9</span>
</Attachment>
<code class={label}>outside</code>
</div>
<div class={cell}>
<Attachment placement="top-end" anchor="inside" offset={4}>
<div class={thumb}></div>
<span slot="content" class={dot}>9</span>
</Attachment>
<code class={label}>inside</code>
</div>
</div>strategy)The content is laid out inside the wrapper, so anything hanging outside the target’s box is clipped by the first ancestor with overflow other than visible — a scrolling rail, an overflow: hidden card. Floating UI computes coordinates; it does not escape ancestor clipping.
Set strategy="fixed" to position the content against the viewport instead. It escapes every clipping ancestor without being portalled out of the DOM, and auto-hides while the target is scrolled out of view. Two caveats: an ancestor with transform, filter, backdrop-filter, contain, or will-change establishes a fixed containing block and clips again, and the content is no longer part of the wrapper’s layout for stacking purposes — raise its z-index through passThrough.content.style if a later sibling paints over it.
<Navigation> already uses strategy="fixed" for its compact-rail badges. If overhang itself is the problem rather than the clip, prefer anchor="inside".
The content is painted on top of the target, so while it’s interactive it also swallows the clicks and hovers that land on its own box — a badge pinned to a link’s corner carves a dead spot out of that link’s hit area. Pass interactive={false} for a decorative indicator (badge, dot, count) and pointer events fall through to the target; keep the default for content that is itself a control, like a dismiss button.
<Navigation>’s compact-rail badges are non-interactive for exactly this reason.
offset stacks on top of the anchor — positive pushes the content further from the target, negative pulls it closer in. Under anchor="inside" it reads as an inward inset instead.
Body text.
---
import Attachment from "../Attachment.astro";
import { css } from "@pindoba/styled-system/css";
const card = css({
width: "240px",
background: "neutral.surface.hill",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "lg",
padding: "md",
});
const title = css({
fontSize: "sm",
fontWeight: "semibold",
color: "neutral.text.bold",
});
const caption = css({
marginTop: "2xs",
color: "neutral.text.muted",
fontSize: "xs",
});
const tag = css({
colorPalette: "primary",
paddingX: "xs",
paddingY: "2xs",
background: "colorPalette",
color: "colorPalette.text.contrast.bold",
borderRadius: "sm",
fontSize: "xs",
fontWeight: "bold",
letterSpacing: "0.05em",
});
---
<Attachment placement="top-start" offset={-8}>
<div class={card}>
<strong class={title}>Card title</strong>
<p class={caption}>Body text.</p>
</div>
<span slot="content" class={tag}>NEW</span>
</Attachment>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.
---
import Attachment from "../Attachment.astro";
import Badge from "@pindoba/astro-badge";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const row = flex({ gap: "2xl", align: "center" });
const thumb = css({
width: "48px",
height: "48px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "md",
});
const thumbRound = css({
width: "48px",
height: "48px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "full",
});
---
<div class={row}>
<Attachment placement="top-end">
<div class={thumb}></div>
<Badge slot="content" size="sm" feedback="danger">3</Badge>
</Attachment>
<Attachment placement="top-end">
<div class={thumb}></div>
<Badge slot="content" size="sm" feedback="danger">99+</Badge>
</Attachment>
<Attachment placement="top-end" shape="circle">
<div class={thumbRound}></div>
<Badge slot="content" size="sm" feedback="danger">New</Badge>
</Attachment>
<Attachment placement="bottom-end" shape="circle">
<div class={thumbRound}></div>
<Badge slot="content" size="sm" feedback="success">Online</Badge>
</Attachment>
</div>Marketing stamps like NEW, -30%, or a rotated SOLD overlay. A negative offset pulls the stamp inward so it overlaps the card edge.
Premium audio, noise cancelling.
Handcrafted, full-grain leather.
Rare 1970s film shooter.
---
import Attachment from "../Attachment.astro";
import Stamp from "@pindoba/astro-stamp";
import { Check, Flame, Star } from "@lucide/astro";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const row = flex({ gap: "2xl", wrap: "wrap" });
const card = css({
width: "240px",
background: "neutral.surface.hill",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "lg",
padding: "md",
});
const title = css({
fontSize: "sm",
fontWeight: "semibold",
color: "neutral.text.bold",
});
const caption = css({
marginTop: "2xs",
color: "neutral.text.muted",
fontSize: "xs",
});
---
<div class={row}>
<Attachment placement="top-start">
<div class={card}>
<strong class={title}>Wireless Headphones</strong>
<p class={caption}>Premium audio, noise cancelling.</p>
</div>
<Stamp slot="content" size="xs" emphasis="primary"><Star /></Stamp>
</Attachment>
<Attachment placement="top-end">
<div class={card}>
<strong class={title}>Leather Wallet</strong>
<p class={caption}>Handcrafted, full-grain leather.</p>
</div>
<Stamp slot="content" emphasis="secondary"><Flame /></Stamp>
</Attachment>
<Attachment placement="top-end">
<div class={card}>
<strong class={title}>Vintage Camera</strong>
<p class={caption}>Rare 1970s film shooter.</p>
</div>
<Stamp slot="content" shape="circle" emphasis="primary" feedback="success"
><Check /></Stamp
>
</Attachment>
</div>"corner"How the attachment is anchored to the target. "corner" centers the attachment on the target corner (badges, dots); "outside" places it flush against the target edge (tooltips, popovers); "inside" tucks it fully within the target's bounds (`offset` becomes an inward inset), so it can't overlap neighbouring rows or a scrollbar.
No description yet.
No description yet.
No description yet.
No description yet.
Flip to the opposite side when overflowing the viewport.
Pull the attachment inward along the diagonal so it hugs the target's rounded corner instead of sitting on the bounding-box corner. Ignored when `shape="circle"`.
trueWhether the content takes pointer events. The content is painted on top of the target, so while it's interactive it also swallows the target's clicks and hovers over its own box — a badge pinned to a link's corner carves a dead spot out of the link. Pass `false` for a decorative indicator (badge, dot, count) so pointer events fall through to the target; keep it `true` when the content is itself a control, like a dismiss button.
0Extra px nudge along the placement axis, stacked on top of the anchor. Under `anchor="inside"` it reads as an inward inset from the target's edges.
0Extra px nudge on the horizontal axis, applied after all positioning.
0Extra px nudge on the vertical axis, applied after all positioning.
0Viewport padding used by the flip/shift middleware.
Per-slot override bag for styling and HTML attributes on each rendered element.
"top-end"Floating UI placement relative to the target's outer bounds.
"rect"Silhouette of the target. Use "circle" so diagonal placements hug the arc of a round target instead of floating off its bounding corner.
Shift along the alignment axis to stay within the viewport.
"absolute"How the content element is positioned. `"absolute"` lays it out inside the attachment root — cheapest, but an ancestor with `overflow` other than `visible` (a scrolling rail, an `overflow: hidden` card) clips the part of the attachment that hangs outside the target. `"fixed"` positions it against the viewport so it escapes every such clip without being portalled out of the DOM, and auto-hides while the target is scrolled out of view. Note that an ancestor with `transform`/`filter`/`contain`/`will-change` establishes a fixed containing block and clips again.
Plus all standard <div> HTML
attributes.