ARM: before-main
BASE: /Users/cindyxz/astryx/worktrees/main @ d4d4fc58b0bd2e21ad7ab23a492edcb4aafa4bc5
DELTA: none; warm main baseline served on :6100
UNRELATED DELTA: none in worktree; compared files unchanged from PR merge-base to origin/main
ARM: after-pr
BASE: /Users/cindyxz/astryx/worktrees/review-pr-5558-badge-truncate @ 1733f9bf784f7a491409e0b59c1d789b83d8ac03
DELTA: final PR diff below (origin/main...HEAD; node_modules symlink was added and removed inside the commit history and is absent at head)
UNRELATED DELTA: evidence files and storybook pid/log are untracked local review artifacts only
.changeset/badge-clamp-long-label.md | 36 +++++++++++++++++++
apps/storybook/stories/Badge.stories.tsx | 62 ++++++++++++++++++++++++++++++++
packages/core/src/Badge/Badge.tsx | 20 ++++++++++-
3 files changed, 117 insertions(+), 1 deletion(-)
diff --git a/.changeset/badge-clamp-long-label.md b/.changeset/badge-clamp-long-label.md
new file mode 100644
index 00000000000..0cddbb1c5f4
--- /dev/null
+++ b/.changeset/badge-clamp-long-label.md
@@ -0,0 +1,36 @@
+---
+'@astryxdesign/core': patch
+---
+
+[fix] Badge: a long label no longer escapes its container.
+
+`Badge` set `white-space: nowrap` with nothing to clip it — the one pairing
+that neither wraps nor truncates. A label wider than the space available
+rendered _outside_ the badge's container and over whatever sat beside it.
+
+```tsx
+
+
+
+```
+
+Measured in Chromium: that badge came out **163px** wide in a 100px column,
+spilling 63px past it; in a fixed-layout table cell it painted 64px over the
+text in the next cell. The badge now clamps to the width it is given and cuts
+the label with an ellipsis.
+
+A badge that already fits is untouched — same width, same height, same DOM.
+Measured before and after, a badge with room to spare is 53px either way; only
+the cases that were already overflowing change. `Badge` uses no hooks and
+stays server-renderable.
+
+The ellipsis sits on an inner label span rather than the badge itself, because
+`text-overflow` needs a block container and taking the root off `inline-flex`
+to get one would cost the icon its centring. With an icon, the icon holds its
+place and the label gives way.
+
+Not included: a tooltip carrying the full text. That needs runtime measurement
+and would make `Badge` a client component, so it is a separate change with its
+own trade-off to weigh.
+
+@freddymeta
diff --git a/apps/storybook/stories/Badge.stories.tsx b/apps/storybook/stories/Badge.stories.tsx
index ada01cb53dc..c26306b59f9 100644
--- a/apps/storybook/stories/Badge.stories.tsx
+++ b/apps/storybook/stories/Badge.stories.tsx
@@ -92,3 +92,65 @@ export const NonSemanticColors: Story = {
),
};
+
+// A badge is one line, so a label wider than the space it has is cut short
+// with an ellipsis rather than escaping the container. Hover a truncated one
+// for the full text.
+export const LongLabels: Story = {
+ name: 'Long labels in tight space',
+ render: () => (
+
+
+
+ In a 100px column
+
+
+
+
+
+
+
+ With an icon — the icon holds its place, the label gives way
+
+
+ ⚠}
+ label="Awaiting security review"
+ />
+
+
+
+
+ Room to spare — unchanged
+
+
+
+
+
+
+ ),
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A badge has a fixed height and never wraps, so a label wider than its container is truncated with an ellipsis rather than escaping the container. A badge that fits is untouched — the clamp only ever changes the case that would otherwise overflow.',
+ },
+ },
+ },
+};
diff --git a/packages/core/src/Badge/Badge.tsx b/packages/core/src/Badge/Badge.tsx
index 14fe58053f3..3fe3a669020 100644
--- a/packages/core/src/Badge/Badge.tsx
+++ b/packages/core/src/Badge/Badge.tsx
@@ -46,6 +46,24 @@ const styles = stylex.create({
lineHeight: typeScaleVars['--text-supporting-leading'],
fontWeight: fontWeightVars['--font-weight-medium'],
whiteSpace: 'nowrap',
+ // A badge is one line by construction — fixed height, `nowrap` — so a
+ // label wider than the space available has to go somewhere. Without these
+ // it went *outside* its container: `nowrap` with nothing to clip it
+ // neither wraps nor truncates, it just escapes, and lands on whatever sits
+ // beside it. `minWidth: 0` matters as much as the max: as a flex item the
+ // automatic minimum size would otherwise hold the badge at its full text
+ // width and push the clamp back out again.
+ maxWidth: '100%',
+ minWidth: 0,
+ },
+ // The ellipsis goes on the label rather than the badge itself, because
+ // `text-overflow` needs a block container and taking the root off
+ // `inline-flex` to get one would cost the icon its centring. The label is a
+ // flex item, so it needs its own `minWidth: 0` for the same reason as above.
+ label: {
+ overflow: 'hidden',
+ textOverflow: 'ellipsis',
+ minWidth: 0,
},
});
@@ -174,7 +192,7 @@ export function Badge({
)}
{...props}>
{icon}
- {label}
+ {label}
);
}