Skip to content

feat(Button): add externalIcon prop#6706

Open
aovee wants to merge 13 commits into
nuxt:v4from
aovee:feat/button-external-icon
Open

feat(Button): add externalIcon prop#6706
aovee wants to merge 13 commits into
nuxt:v4from
aovee:feat/button-external-icon

Conversation

@aovee

@aovee aovee commented Jul 9, 2026

Copy link
Copy Markdown

🔗 Linked issue

Resolves #4501

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

Added externalcon prop to Button (similar to NavigationMenu. By default (and by design, I guess), it's disabled, and only enabled if target="_blank" and externalIcon is true or references any Icon.
In order to have the same "superscript" effect as NavigationItem, I also added the externalIcon and externalIconContainer slots to Button.

Change is not required per say, but can be useful for some people.

I also added examples in the docs but I'm not sure it is that useful. However, the feature that externalIcon can be any Icon isn't documented in NavigationMenu.

P.S.: please, keep in mind that this is my first contribution and PR, so don't hesitate to point out what's wrong, missing or too much; I'm completely open to feedbacks.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@aovee aovee requested a review from benjamincanac as a code owner July 9, 2026 14:01
@github-actions github-actions Bot added the v4 #4488 label Jul 9, 2026
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c8e05ac9-a41d-4d40-8c88-386b821825a1

📥 Commits

Reviewing files that changed from the base of the PR and between 9fd624b and 7347121.

⛔ Files ignored due to path filters (2)
  • test/components/__snapshots__/Button-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Button.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (2)
  • src/runtime/components/Button.vue
  • test/components/Button.spec.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/components/Button.spec.ts
  • src/runtime/components/Button.vue

📝 Walkthrough

Walkthrough

This change adds an externalIcon prop to the Button component for links with target="_blank". The prop accepts a boolean or icon name and defaults to false. Button theme slots and per-size classes were updated for the external icon and trailing icon layouts. Tests, documentation, and a playground example were added for the new behavior.

Estimated code review effort: 2 (Simple) | ~12 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR covers UButton, but the linked issue also requests ULink support and no ULink change is shown. Add the external icon option to ULink as requested, or clarify the issue scope if only UButton is intended.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding an externalIcon prop to Button.
Description check ✅ Passed The description is clearly related to the Button external icon feature and documentation changes.
Out of Scope Changes check ✅ Passed The docs, playground example, theme slots, and tests all support the externalIcon feature and appear in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/runtime/components/Button.vue (1)

162-163: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Minor: inconsistent array wrapping for class parameter.

The new externalIconContainer and externalIcon slots wrap props.ui?.* in array brackets, while existing slots (leadingIcon, trailingIcon) pass the value directly. Both work, but matching the existing style keeps the template consistent.

♻️ Suggested consistency fix
-      <div v-if="props.target === '_blank' && props.externalIcon !== false" data-slot="externalIconContainer" :class="ui.externalIconContainer({ class: [props.ui?.externalIconContainer], active })">
-        <UIcon :name="typeof props.externalIcon === 'string' ? props.externalIcon : appConfig.ui.icons.external" data-slot="externalIcon" :class="ui.externalIcon({ class: [props.ui?.externalIcon], active })" />
+      <div v-if="props.target === '_blank' && props.externalIcon !== false" data-slot="externalIconContainer" :class="ui.externalIconContainer({ class: props.ui?.externalIconContainer, active })">
+        <UIcon :name="typeof props.externalIcon === 'string' ? props.externalIcon : appConfig.ui.icons.external" data-slot="externalIcon" :class="ui.externalIcon({ class: props.ui?.externalIcon, active })" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/components/Button.vue` around lines 162 - 163, The Button
template has inconsistent class binding style for the new external icon slots.
Update the `externalIconContainer` and `externalIcon` bindings in `Button.vue`
to match the existing `leadingIcon` and `trailingIcon` pattern by passing
`props.ui?.externalIconContainer` and `props.ui?.externalIcon` directly through
the `ui.*({ class: ... })` calls in the same way as the other icon slots.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/components/Button.spec.ts`:
- Around line 27-29: The Button spec has a duplicate case because the “with
externalIcon” scenario omits the prop and behaves the same as `externalIcon:
false`. Update the first test case in `Button.spec.ts` to pass `externalIcon:
true` so the `Button`/link rendering path is actually exercised, and keep the
existing `externalIcon: false` and custom icon cases to cover the other
behaviors.

---

Nitpick comments:
In `@src/runtime/components/Button.vue`:
- Around line 162-163: The Button template has inconsistent class binding style
for the new external icon slots. Update the `externalIconContainer` and
`externalIcon` bindings in `Button.vue` to match the existing `leadingIcon` and
`trailingIcon` pattern by passing `props.ui?.externalIconContainer` and
`props.ui?.externalIcon` directly through the `ui.*({ class: ... })` calls in
the same way as the other icon slots.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c6816011-8063-492f-992c-a98150eb0e5d

📥 Commits

Reviewing files that changed from the base of the PR and between 71c623b and 9fd624b.

⛔ Files ignored due to path filters (80)
  • test/components/__snapshots__/Alert-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Alert.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/AuthForm-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/AuthForm.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Banner-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Banner.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Button-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Button.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Calendar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Calendar.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Carousel-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Carousel.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/ChatPromptSubmit-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/ChatPromptSubmit.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/ChatTool-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/ChatTool.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/CommandPalette-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/CommandPalette.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardNavbar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardNavbar.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSearch-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSearch.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSearchButton-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSearchButton.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebar.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebarCollapse-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebarCollapse.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebarToggle-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/DashboardSidebarToggle.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Drawer-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Drawer.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/EditorDragHandle-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/EditorDragHandle.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/EditorToolbar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/EditorToolbar.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Empty-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Empty.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Error-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Error.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/FieldGroup-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/FieldGroup.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/FileUpload-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/FileUpload.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Header-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Header.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/InputMenu-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/InputMenu.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/InputNumber-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/InputNumber.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Modal-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Modal.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageCTA-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageCTA.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageHeader-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageHeader.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageHero-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageHero.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageSection-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PageSection.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Pagination-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Pagination.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PricingPlan-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PricingPlan.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PricingTable-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/PricingTable.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/SelectMenu.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Sidebar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Sidebar.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Slideover-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Slideover.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Table-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Table.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Theme-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Theme.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Toast-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Toast.spec.ts.snap is excluded by !**/*.snap
  • test/components/content/__snapshots__/ContentSearch.spec.ts.snap is excluded by !**/*.snap
  • test/components/content/__snapshots__/ContentSearchButton.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (5)
  • docs/content/docs/2.components/button.md
  • playgrounds/nuxt/app/pages/components/button.vue
  • src/runtime/components/Button.vue
  • src/theme/button.ts
  • test/components/Button.spec.ts

Comment thread test/components/Button.spec.ts Outdated
@pkg-pr-new

pkg-pr-new Bot commented Jul 9, 2026

Copy link
Copy Markdown
npm i https://pkg.pr.new/@nuxt/ui@6706

commit: 0aa9566

@aovee

aovee commented Jul 9, 2026

Copy link
Copy Markdown
Author

I didn't add the externalIcon prop to Link because it seemed like a too impactful change considering no icon or slot are handled in this component for now.

I found it better to make it available only to Button for now and then, if approved, extend it to Link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add external icon option to UButton and ULink

1 participant