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.
---
// GENERATED from packages/demos/attachment/default.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
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">
<span slot="content" class={status}></span>
<div class={avatar}></div>
</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---
// GENERATED from packages/demos/attachment/placements.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
import { css } from "@pindoba/styled-system/css";
import { flex } from "@pindoba/styled-system/patterns";
const placements = [
"top-start",
"top-end",
"bottom-start",
"bottom-end",
] as const;
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}>
<span slot="content" class={dot}>
3
</span>
<div class={thumb}></div>
</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---
// GENERATED from packages/demos/attachment/anchor.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
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">
<span slot="content" class={dot}>
9
</span>
<div class={thumb}></div>
</Attachment>
<code class={label}>corner</code>
</div>
<div class={cell}>
<Attachment placement="top-end" anchor="outside">
<span slot="content" class={dot}>
9
</span>
<div class={thumb}></div>
</Attachment>
<code class={label}>outside</code>
</div>
<div class={cell}>
<Attachment placement="top-end" anchor="inside" offset={4}>
<span slot="content" class={dot}>
9
</span>
<div class={thumb}></div>
</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".
strategy="inline")Both strategies above measure the target with Floating UI and re-measure after layout changes, so when the target moves inside a layout animation — a sidebar collapsing, a panel morphing — the attachment trails it by a frame. Set strategy="inline" and the placement is pure CSS instead: top/left percentages of the wrapper (which shrink-wraps the target) plus a translate of the content’s own size. No JavaScript runs, the content is in place in the server-rendered markup on first paint, and it moves in the same frame as its target.
It supports every placement, anchor, shape="circle" (exact for square or round targets), offset, offsetX, offsetY, and interactive. It ignores what needs a measurement — hugCorners, flip, shift, and trackTransform — and warns in development if you pass them. Use it for in-flow decorations such as badges, dots, and counts; keep "absolute" or "fixed" when you need collision handling or to escape a clip.
cornerinsidecircle---
// GENERATED from packages/demos/attachment/inline.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
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 avatar = css({
width: "64px",
height: "64px",
background: "neutral.surface.ground",
border: "1px solid",
borderColor: "neutral.border.muted",
borderRadius: "full",
});
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 status = css({
colorPalette: "success",
width: "14px",
height: "14px",
background: "colorPalette",
border: "2px solid",
borderColor: "neutral.surface.base",
borderRadius: "full",
});
const label = css({ fontSize: "xs" });
---
<div class={row}>
<div class={cell}>
<Attachment strategy="inline" placement="top-end" interactive={false}>
<span slot="content" class={dot}>
9
</span>
<div class={thumb}></div>
</Attachment>
<code class={label}>corner</code>
</div>
<div class={cell}>
<Attachment
strategy="inline"
placement="bottom-start"
anchor="inside"
offset={4}
interactive={false}
>
<span slot="content" class={dot}>
9
</span>
<div class={thumb}></div>
</Attachment>
<code class={label}>inside</code>
</div>
<div class={cell}>
<Attachment
strategy="inline"
placement="bottom-end"
shape="circle"
interactive={false}
>
<span slot="content" class={status}></span>
<div class={avatar}></div>
</Attachment>
<code class={label}>circle</code>
</div>
</div>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.
---
// GENERATED from packages/demos/attachment/offset.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
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}>
<span slot="content" class={tag}>
NEW
</span>
<div class={card}>
<strong class={title}>Card title</strong>
<p class={caption}>Body text.</p>
</div>
</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.
---
// GENERATED from packages/demos/attachment/badges.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
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">
<Badge slot="content" size="sm" tone="danger">
3
</Badge>
<div class={thumb}></div>
</Attachment>
<Attachment placement="top-end">
<Badge slot="content" size="sm" tone="danger">
99+
</Badge>
<div class={thumb}></div>
</Attachment>
<Attachment placement="top-end" shape="circle">
<Badge slot="content" size="sm" tone="danger">
New
</Badge>
<div class={thumbRound}></div>
</Attachment>
<Attachment placement="bottom-end" shape="circle">
<Badge slot="content" size="sm" tone="success">
Online
</Badge>
<div class={thumbRound}></div>
</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.
---
// GENERATED from packages/demos/attachment/stamps.demo.tsx — edit the source, not this file.
import Attachment from "@pindoba/astro-attachment";
import Stamp from "@pindoba/astro-stamp";
import { Star, Flame, Check } 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">
<Stamp slot="content" size="xs" emphasis="primary">
<Star />
</Stamp>
<div class={card}>
<strong class={title}>Wireless Headphones</strong>
<p class={caption}>Premium audio, noise cancelling.</p>
</div>
</Attachment>
<Attachment placement="top-end">
<Stamp slot="content" emphasis="secondary">
<Flame />
</Stamp>
<div class={card}>
<strong class={title}>Leather Wallet</strong>
<p class={caption}>Handcrafted, full-grain leather.</p>
</div>
</Attachment>
<Attachment placement="top-end">
<Stamp slot="content" shape="circle" emphasis="primary" tone="success">
<Check />
</Stamp>
<div class={card}>
<strong class={title}>Vintage Camera</strong>
<p class={caption}>Rare 1970s film shooter.</p>
</div>
</Attachment>
</div>