[CLOV-1337][BpkButton] Add leadingIcon and trailingIcon props#4269
[CLOV-1337][BpkButton] Add leadingIcon and trailingIcon props#4269Richard-Shen (RichardSyq) wants to merge 14 commits intomainfrom
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
There was a problem hiding this comment.
Pull request overview
Adds support for optional leading and trailing icons to BpkButtonV2, including styling and Storybook/Figma examples, plus updated tests to cover the new API.
Changes:
- Extend
BpkButtonV2props withleadingIconandtrailingIcon, and render them with new BEM element wrappers. - Add styling for icon layout (
--has-icon,__leading-icon,__trailing-icon) and full-width alignment. - Update accessibility/unit tests and add Storybook + Figma Code Connect examples for the new icon props.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/bpk-component-button/src/BpkButtonV2/common-types.tsx | Adds leadingIcon/trailingIcon to the component props type. |
| packages/bpk-component-button/src/BpkButtonV2/BpkButton.tsx | Renders optional leading/trailing icons and applies a --has-icon modifier. |
| packages/bpk-component-button/src/BpkButtonV2/BpkButton.module.scss | Adds layout rules for icon-bearing buttons and icon wrappers. |
| packages/bpk-component-button/src/BpkButtonV2/accessibility-test.tsx | Adds axe coverage for leading/trailing icon configurations. |
| packages/bpk-component-button/src/BpkButtonV2/BpkButton-test.tsx | Adds unit tests for icon rendering and class application (incl. snapshots). |
| packages/bpk-component-button/src/BpkButtonV2/BpkButton.figma.tsx | Updates Code Connect props to use leadingIcon/trailingIcon. |
| examples/bpk-component-button/stories.tsx | Adds new Storybook stories/visual test entries for icon props. |
| examples/bpk-component-button/examples.tsx | Adds Storybook example components that exercise the new icon props. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
packages/bpk-component-button/src/BpkButtonV2/BpkButton-test.tsx
Outdated
Show resolved
Hide resolved
packages/bpk-component-button/src/BpkButtonV2/BpkButton.figma.tsx
Outdated
Show resolved
Hide resolved
packages/bpk-component-button/src/BpkButtonV2/BpkButton.figma.tsx
Outdated
Show resolved
Hide resolved
| &--full-width#{&}--has-icon { | ||
| display: flex; | ||
| justify-content: center; | ||
| } |
There was a problem hiding this comment.
The selector &--full-width#{&}--has-icon works, but the interpolation makes it harder to read/maintain. This can be expressed more clearly as &--full-width&--has-icon (same combined selector) while avoiding string interpolation.
There was a problem hiding this comment.
The current version of sass-loader does not support &--full-width&--has-icon
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
…railingIcon props Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
- Use figma.boolean for leadingIcon, trailingIcon and iconOnly props - Move leadingIcon/trailingIcon adjacent in props destructuring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
Richard-Shen (RichardSyq)
left a comment
There was a problem hiding this comment.
Code review
Found 1 issue:
- Non-semantic spacing token used for icon-to-text gap —
$bpk-spacing-icon-text: .5remexists in backpack-foundations specifically for icon-to-text spacing and produces the same value asbpk-spacing-md(). Using the size-based function instead of the semantic variable breaks the principle stated in the Constitution and AGENTS.md: "Use semantic color tokens that describe purpose, not appearance" — the same applies to spacing. Replacegap: tokens.bpk-spacing-md()withgap: tokens.$bpk-spacing-icon-text.
backpack/packages/bpk-component-button/src/BpkButtonV2/BpkButton.module.scss
Lines 113 to 118 in 93a2566
🤖 Generated with Claude Code
- If this code review was useful, please react with 👍. Otherwise, react with 👎.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| iconOnly: figma.boolean('Icon only') | ||
| }, | ||
| example: ({ iconOnly, isDisabled, label, leftContent, rightContent, size, style }) => ( | ||
| <BpkButton type={style} size={size} disabled={isDisabled} iconOnly={iconOnly}> | ||
| {leftContent} | ||
| example: ({ iconOnly, isDisabled, label, leadingIcon, size, style, trailingIcon }) => ( | ||
| <BpkButton | ||
| type={style} | ||
| size={size} | ||
| disabled={isDisabled} | ||
| iconOnly={iconOnly} | ||
| leadingIcon={leadingIcon} | ||
| trailingIcon={trailingIcon} | ||
| > | ||
| {label} | ||
| {rightContent} | ||
| </BpkButton> |
There was a problem hiding this comment.
In the Code Connect example, iconOnly is passed through but the button still renders {label} as children and doesn’t provide an accessible name. When iconOnly is true the rendered output will be an “icon-only styled” button that still contains text, and (if you switch to rendering only an icon) it would also need aria-label (e.g., derived from label). Consider branching the example so that iconOnly=true renders an icon as children and sets aria-label={label}, and also avoids passing leadingIcon/trailingIcon in that mode.
There was a problem hiding this comment.
Fixed. The example is now branched on iconOnly:
When iconOnly=true: renders as children, sets aria-label={label}, and omits leadingIcon/trailingIcon
When iconOnly=false: renders {label} as children with leadingIcon/trailingIcon passed through as normal
However, no components under packages/ use $bpk-spacing-icon-text, so I think we should continue using bpk-spacing-md(). |
…Icon/trailingIcon - README: add leadingIcon/trailingIcon usage examples - button-link-type.md: update With Icons section to show new props as recommended approach; deprecate icon-as-children-sibling pattern; restore Custom Text Styling with BpkText with migration note - BpkButton.figma.tsx: branch iconOnly example to render icon as children with aria-label, avoiding invalid leadingIcon/trailingIcon usage in icon-only mode Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
Gert-Jan Vercauteren (gert-janvercauteren)
left a comment
There was a problem hiding this comment.
Implementation looks alright! Just a catch on hovering on the Link type button. (This is somewhat pre-existing, but would be good to resolve right here and now :D )
| rel={rel} | ||
| {...rest} | ||
| > | ||
| {leadingIconEl} |
There was a problem hiding this comment.
| | ||
| <Wrapped | ||
| size={SIZE_TYPES.large} | ||
| leadingIcon={<SmallLightningIcon />} |
There was a problem hiding this comment.
Nit: Maybe we should use large icon here?
There was a problem hiding this comment.
Thanks you reminder, I have modified this part.
| @@ -35,16 +35,19 @@ export const BpkButtonV2 = ({ | |||
| href = null, | |||
| iconOnly = false, | |||
There was a problem hiding this comment.
That's a good question! I'll consider this situation.
There was a problem hiding this comment.
I believe the two newly added props should not affect the case where iconOnly=true. I've configured it so that leading icons and trailing icons are not rendered when iconOnly=true.
There was a problem hiding this comment.
Is there a way to add typesafety where if iconOnly = true, then leading/trailing prop are not allowed?
There was a problem hiding this comment.
Gert-Jan Vercauteren (@gert-janvercauteren) I added this condition; do you think it's suitable as a typesafety?
- Use LargeLightningIcon for large button examples instead of small - Fix link button hover animation to trigger from root button hover (not just text span) using CSS variables - Ignore leadingIcon/trailingIcon when iconOnly=true to prevent three-icon rendering Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
…er selectors - Replace CSS variable approach with pure descendant hover selectors to avoid overriding dark mode underline styles - Only apply hover animation override when icons are present (--has-icon) - Remove link/linkOnDark anchor tag href from AnchorTagsExample to avoid pre-existing vertical centering issue with <a> tags Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
|
Visit https://backpack.github.io/storybook-prs/4269 to see this build running in a browser. |
Gert-Jan Vercauteren (gert-janvercauteren)
left a comment
There was a problem hiding this comment.
One last thing and then this feels good to go!
| rel={rel} | ||
| {...rest} | ||
| > | ||
| {leadingIconEl} |
There was a problem hiding this comment.
We should consider treating content for both link and button the same, should we extract it out and then put in? (Considering there's now conditional rendering for button but not for



Summary
Adds leadingIcon and trailingIcon optional props to BpkButtonV2, providing dedicated icon slots before and after the button label.
Previously, icons had to be passed via children with manual alignment using withButtonAlignment(). The new props offer a cleaner API with automatic flexbox alignment, consistent spacing, and no wrapper needed.
Changes
common-types.tsx — added leadingIcon?: ReactNode and trailingIcon?: ReactNode to Props
BpkButton.tsx — renders icon wrapper spans, applies bpk-button--has-icon class when icons are present
BpkButton.module.scss — flexbox layout with gap for spacing; text-decoration: none on icon wrappers to prevent underline bleeding on link buttons
BpkButton.figma.tsx — updated Code Connect to use figma.boolean for leadingIcon, trailingIcon and iconOnly
BpkButton-test.tsx — DOM assertion tests for icon prop variants
examples/ — all button type stories updated to use leadingIcon/trailingIcon props; removed old children-based icon pattern
Remember to include the following changes:
[Clover-123][BpkButton] Updating the colourREADME.md(If you have created a new component)README.md