Skip to content

Conversation

@yeonjulee1005
Copy link
Contributor

@yeonjulee1005 yeonjulee1005 commented Nov 15, 2025

πŸ”— 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)
  • ✨ 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, and right
  • 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
2025-11-15.10.17.50.mov

Backward Compatibility:

  • Maintains full backward compatibility with existing code
  • Existing usage with leading and trailing props continues to work without any changes
  • When loadingPosition is not specified, the component automatically determines the position based on the trailing prop

Implementation Details:

  • Added loadingPosition prop to ButtonProps interface
  • Updated useComponentIcons logic 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

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

Add 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. The prop accepts three values:
- '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

The implementation maintains full backward compatibility. Existing usage
with `leading` and `trailing` props for loading buttons continues to
work without any changes. When `loadingPosition` is not specified, the
component automatically determines the position based on the `trailing`
prop, preserving the existing behavior.

Adds test cases for all three loadingPosition values to ensure proper
rendering and behavior.
Add documentation section for the new `loading-position` prop in the
Button component. The section includes:
- Description of the three position options (left, center, right)
- Code examples for each position option
- Backward compatibility note explaining that existing usage with
  `leading` and `trailing` props continues to work

This documentation helps developers understand how to control the
loading icon placement when using the Button component in loading state.
@github-actions github-actions bot added the v4 #4488 label Nov 15, 2025
@pkg-pr-new
Copy link

pkg-pr-new bot commented Nov 15, 2025

npm i https://pkg.pr.new/@nuxt/ui@5456

commit: db780e9

@yeonjulee1005
Copy link
Contributor Author

Thank you! :)

@rijenkii
Copy link
Contributor

rijenkii commented Nov 15, 2025

First off -- thank you. I have some comments and questions.

  1. 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 in useComponentIcons -- 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 between Badge, Input*, Select* and Textarea, and it may be useful there as well.

  2. In the documentation you say:

    Use the loading-position prop 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:

    ScreenshotScreenshot 2025-11-15 at 12-55-15
    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>
  3. No sure whether left/right is better than leading/trailing -- firstly because it introduces new language when leading/trailing is already there, secondly because of potential issues with RTL locales, but not sure about that one.

@yeonjulee1005
Copy link
Contributor Author

@rijenkii

Questions are always welcome!πŸ€—

  1. That’s right! There are other components that also receive loading prop.
    However, since these components don’t handle events like buttons do, I believe the provided value should always be displayed by default.

  2. 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 loadingPosition should still allow users to specify the intended placement.

  3. Other components also use left and right as prop values, so I felt that using those(include center also) would be clearer and more consistent than using leading or trailing.

@rijenkii
Copy link
Contributor

I just looked at the implementation, and loading-position="center" does not do what I described in #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):

Screencast.From.2025-11-15.15-55-30.mp4

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):

Screencast.From.2025-11-15.15-57-40.mp4

@yeonjulee1005
Copy link
Contributor Author

yeonjulee1005 commented Nov 15, 2025

@rijenkii
Sure~! I’ll update it so that the changes are applied.

2025-11-15.8.27.12.mov

When loadingPosition is set to 'center' and a label is provided, the button now maintains the label's width even though the label is hidden. The loading icon is displayed in the center of the button while preserving the original button width.

Changes:
- Add loadingIcon and loadingLabel slots to button theme
- Add animate-spin class to loadingIcon when loading is true
- Update Button component to render label with opacity-0 and h-0 when loadingPosition is center
- Update test snapshots to reflect the new structure
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.

2 participants