diff --git a/.gitignore b/.gitignore
index 791af041b7..4794e3ca67 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,7 @@ logs
.DS_Store
.fleet
.idea
+.vscode
# Local env files
.env
diff --git a/docs/content/docs/2.components/button.md b/docs/content/docs/2.components/button.md
index 1aa5258525..3157eb679b 100644
--- a/docs/content/docs/2.components/button.md
+++ b/docs/content/docs/2.components/button.md
@@ -283,6 +283,47 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.lo
:::
::
+### Loading Position
+
+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)
+- `center`: displays loading icon in the center, hiding the label
+- `right`: displays loading icon on the right
+
+::component-code
+---
+props:
+ loading: true
+ loadingPosition: 'left'
+ label: Button
+---
+Button
+::
+
+::component-code
+---
+props:
+ loading: true
+ loadingPosition: 'center'
+ label: Button
+---
+Button
+::
+
+::component-code
+---
+props:
+ loading: true
+ loadingPosition: 'right'
+ label: Button
+---
+Button
+::
+
+:::tip
+The `loading-position` prop maintains full backward compatibility. Existing usage with `leading` and `trailing` props for loading buttons continues to work without any changes. When `loading-position` is not specified, the component automatically determines the position based on the `trailing` prop.
+:::
+
### Disabled
Use the `disabled` prop to disable the Button.
diff --git a/playgrounds/nuxt/app/pages/components/button.vue b/playgrounds/nuxt/app/pages/components/button.vue
index a8d25610eb..b733ad23c2 100644
--- a/playgrounds/nuxt/app/pages/components/button.vue
+++ b/playgrounds/nuxt/app/pages/components/button.vue
@@ -28,8 +28,9 @@ function onClick() {
-
-
+
+
+
diff --git a/src/runtime/components/Button.vue b/src/runtime/components/Button.vue
index defb42f768..69201074d9 100644
--- a/src/runtime/components/Button.vue
+++ b/src/runtime/components/Button.vue
@@ -30,6 +30,11 @@ export interface ButtonProps extends UseComponentIconsProps, Omit void | Promise) | Array<((event: MouseEvent) => void | Promise)>
class?: any
ui?: Button['slots']
@@ -83,9 +88,36 @@ const isLoading = computed(() => {
return props.loading || (props.loadingAuto && (loadingAutoState.value || (formLoading?.value && props.type === 'submit')))
})
-const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(
- computed(() => ({ ...props, loading: isLoading.value }))
-)
+const loadingPosition = computed(() => {
+ if (props.loadingPosition) {
+ return props.loadingPosition
+ }
+ return props.trailing ? 'right' : 'left'
+})
+
+const iconProps = computed(() => {
+ const baseProps = { ...props, loading: isLoading.value }
+
+ if (isLoading.value) {
+ if (loadingPosition.value === 'right') {
+ return { ...baseProps, trailing: true }
+ } else if (loadingPosition.value === 'left') {
+ return { ...baseProps, trailing: false }
+ }
+ return { ...baseProps, trailing: false }
+ }
+
+ return baseProps
+})
+
+const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(iconProps)
+
+const centerLoadingIcon = computed(() => {
+ if (isLoading.value && loadingPosition.value === 'center') {
+ return props.loadingIcon || appConfig.ui.icons.loading
+ }
+ return undefined
+})
const ui = computed(() => tv({
extend: tv(theme),
@@ -126,7 +158,7 @@ const ui = computed(() => tv({
v-bind="slotProps"
data-slot="base"
:class="ui.base({
- class: [props.ui?.base, props.class],
+ class: [props.ui?.base, props.class, ...(loadingPosition === 'center' && isLoading ? ['relative'] : [])],
active,
...(active && activeVariant ? { variant: activeVariant } : {}),
...(active && activeColor ? { color: activeColor } : {})
@@ -134,18 +166,28 @@ const ui = computed(() => tv({
@click="onClickWrapper"
>
-
-
+
+
-
- {{ label }}
-
+
+
+
+ {{ label }}
+
+
+
+
+
+
+ {{ label }}
+
+
-
+
diff --git a/src/theme/button.ts b/src/theme/button.ts
index 12998dad05..25aae3f98a 100644
--- a/src/theme/button.ts
+++ b/src/theme/button.ts
@@ -8,7 +8,9 @@ export default (options: Required) => ({
leadingIcon: 'shrink-0',
leadingAvatar: 'shrink-0',
leadingAvatarSize: '',
- trailingIcon: 'shrink-0'
+ trailingIcon: 'shrink-0',
+ loadingIcon: 'shrink-0 size-5',
+ loadingLabel: 'opacity-0 h-0'
},
variants: {
...fieldGroupVariant,
@@ -164,6 +166,11 @@ export default (options: Required) => ({
class: {
trailingIcon: 'animate-spin'
}
+ }, {
+ loading: true,
+ class: {
+ loadingIcon: 'animate-spin'
+ }
}],
defaultVariants: {
color: 'primary',
diff --git a/test/components/Button.spec.ts b/test/components/Button.spec.ts
index 75afdf4f23..c98877e446 100644
--- a/test/components/Button.spec.ts
+++ b/test/components/Button.spec.ts
@@ -32,6 +32,9 @@ describe('Button', () => {
['with loading and avatar', { props: { loading: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }],
['with loading trailing', { props: { loading: true, trailing: true } }],
['with loading trailing and avatar', { props: { loading: true, trailing: true, avatar: { src: 'https://github.com/benjamincanac.png' } } }],
+ ['with loadingPosition left', { props: { loading: true, loadingPosition: 'left', label: 'Button' } }],
+ ['with loadingPosition center', { props: { loading: true, loadingPosition: 'center', label: 'Button' } }],
+ ['with loadingPosition right', { props: { loading: true, loadingPosition: 'right', label: 'Button' } }],
['with loadingIcon', { props: { loading: true, loadingIcon: 'i-lucide-loader' } }],
['with disabled', { props: { label: 'Button', disabled: true } }],
['with disabled and with link', { props: { label: 'Button', disabled: true, to: '/link' } }],
diff --git a/test/components/__snapshots__/Button-vue.spec.ts.snap b/test/components/__snapshots__/Button-vue.spec.ts.snap
index b899958e59..cf182a2855 100644
--- a/test/components/__snapshots__/Button-vue.spec.ts.snap
+++ b/test/components/__snapshots__/Button-vue.spec.ts.snap
@@ -187,6 +187,26 @@ exports[`Button > renders with loadingIcon correctly 1`] = `
"
`;
+exports[`Button > renders with loadingPosition center correctly 1`] = `
+""
+`;
+
+exports[`Button > renders with loadingPosition left correctly 1`] = `
+""
+`;
+
+exports[`Button > renders with loadingPosition right correctly 1`] = `
+""
+`;
+
exports[`Button > renders with neutral variant ghost correctly 1`] = `
""
`;
+exports[`Button > renders with loadingPosition center correctly 1`] = `
+""
+`;
+
+exports[`Button > renders with loadingPosition left correctly 1`] = `
+""
+`;
+
+exports[`Button > renders with loadingPosition right correctly 1`] = `
+""
+`;
+
exports[`Button > renders with neutral variant ghost correctly 1`] = `
"