feat(button): add loadingPosition prop for loading icon placement control
๐ Linked issue
#5362
โ 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)
- [x] โจ 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
Adds a new loadingPosition prop to the Button component that allows developers to control where the loading icon appears when the button is in a loading state.
Features:
- Three position options:
left,center, andright -
left: displays loading icon on the left (default behavior) -
center: displays loading icon in the center, hiding the label -
right: displays loading icon on the right
https://github.com/user-attachments/assets/818c22ed-09dd-4a54-87ff-085fc5ce4b09
Backward Compatibility:
- Maintains full backward compatibility with existing code
- Existing usage with
leadingandtrailingprops continues to work without any changes - When
loadingPositionis not specified, the component automatically determines the position based on thetrailingprop
Implementation Details:
- Added
loadingPositionprop toButtonPropsinterface - Updated
useComponentIconslogic to handle position-based loading icon placement - Added conditional rendering for center position (hides label, shows only loading icon)
- Added comprehensive test cases for all three position options
Documentation:
- Added new "Loading Position" section in button.md
- Includes code examples for each position option
- Added backward compatibility note
๐ Checklist
- [x] I have linked an issue or discussion.
- [x] I have updated the documentation accordingly.
Thank you! :)
First off -- thank you. I have some comments and questions.
-
I'm not sure whether this needs to be a
UButton-specific feature, or if it could be useful for other components and should instead be implemented inuseComponentIcons-- when I was making the issue I have not read icon positioning source code, but now that I have I see that the icon positioning logic is reused betweenBadge,Input*,Select*andTextarea, and it may be useful there as well. -
In the documentation you say:
Use the
loading-positionprop to control where the loading icon appears. The prop accepts three values:-
left: displays loading icon on the left (default)
In but in the issue you say:
Backward Compatibility:
- When loadingPosition is not specified, the component automatically determines the position based on the trailing prop
Which one is it? Current behaviour is not putting the loading spinner at the start of the button, but instead follows a complex logic that I was not able to simplify much yet:
Screenshot
Source
<script setup lang="ts"> const args: { icon?: string; leading?: boolean; trailing?: boolean; leadingIcon?: string; trailingIcon?: string; }[] = [ {}, { trailingIcon: "i-lucide-chevron-right" }, { trailing: true }, { trailing: true, trailingIcon: "i-lucide-chevron-right" }, { leadingIcon: "i-lucide-chevron-left" }, { leadingIcon: "i-lucide-chevron-left", trailingIcon: "i-lucide-chevron-right", }, { leadingIcon: "i-lucide-chevron-left", trailing: true }, { leadingIcon: "i-lucide-chevron-left", trailing: true, trailingIcon: "i-lucide-chevron-right", }, { leading: true }, { leading: true, trailingIcon: "i-lucide-chevron-right" }, { leading: true, trailing: true }, { leading: true, trailing: true, trailingIcon: "i-lucide-chevron-right" }, { leading: true, leadingIcon: "i-lucide-chevron-left" }, { leading: true, leadingIcon: "i-lucide-chevron-left", trailingIcon: "i-lucide-chevron-right", }, { leading: true, leadingIcon: "i-lucide-chevron-left", trailing: true }, { leading: true, leadingIcon: "i-lucide-chevron-left", trailing: true, trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right" }, { icon: "i-lucide-chevrons-left-right", trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", trailing: true }, { icon: "i-lucide-chevrons-left-right", trailing: true, trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leadingIcon: "i-lucide-chevron-left", }, { icon: "i-lucide-chevrons-left-right", leadingIcon: "i-lucide-chevron-left", trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leadingIcon: "i-lucide-chevron-left", trailing: true, }, { icon: "i-lucide-chevrons-left-right", leadingIcon: "i-lucide-chevron-left", trailing: true, trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leading: true }, { icon: "i-lucide-chevrons-left-right", leading: true, trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leading: true, trailing: true }, { icon: "i-lucide-chevrons-left-right", leading: true, trailing: true, trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leading: true, leadingIcon: "i-lucide-chevron-left", }, { icon: "i-lucide-chevrons-left-right", leading: true, leadingIcon: "i-lucide-chevron-left", trailingIcon: "i-lucide-chevron-right", }, { icon: "i-lucide-chevrons-left-right", leading: true, leadingIcon: "i-lucide-chevron-left", trailing: true, }, { icon: "i-lucide-chevrons-left-right", leading: true, leadingIcon: "i-lucide-chevron-left", trailing: true, trailingIcon: "i-lucide-chevron-right", }, ]; </script> <template> <div class="flex flex-col gap-2 p-4"> <div v-for="(arg, key) of args" :key class="flex gap-2"> <UButton label="Label" :icon="arg.icon" :leading="arg.leading" :trailing="arg.trailing" :leading-icon="arg.leadingIcon" :trailing-icon="arg.trailingIcon" /> <UButton label="Label" loading :icon="arg.icon" :leading="arg.leading" :trailing="arg.trailing" :leading-icon="arg.leadingIcon" :trailing-icon="arg.trailingIcon" /> <code>{{ arg }}</code> </div> </div> </template> -
-
No sure whether
left/rightis better thanleading/trailing-- firstly because it introduces new language whenleading/trailingis already there, secondly because of potential issues with RTL locales, but not sure about that one.
@rijenkii
Questions are always welcome!๐ค
-
Thatโs right! There are other components that also receive
loadingprop. However, since these components donโt handle events like buttons do, I believe the providedvalue should always be displayed by default. -
I wanted to make sure existing users donโt get confused. So while keeping the current logic where leading and trailing are chosen when using loading, I thought adding
loadingPositionshould still allow users to specify the intended placement. -
Other components also use left and right as prop values, so I felt that using those(include
centeralso) would be clearer and more consistent than using leading or trailing.
I just looked at the implementation, and loading-position="center" does not do what I described in https://github.com/nuxt/ui/issues/5362: it does not keep the width of the button intact when toggling the loading.
The implemented behaviour (label disappears completely and is replaced by the loading icon, changing the width of the button):
https://github.com/user-attachments/assets/d75581a3-2c64-4698-9fc4-2511664e323f
The behaviour described in the issue (label and other icons receive opacity: 0 with the spinner rendered above them, keeping the width of the button):
https://github.com/user-attachments/assets/e21e8eeb-ffd9-4260-9737-d166d9890eb3
@rijenkii Sure~! Iโll update it so that the changes are applied.
https://github.com/user-attachments/assets/21494528-e4a5-41cf-99dc-5bb47feace01