-
Notifications
You must be signed in to change notification settings - Fork 933
fix(ColorModeButton): use css to detect color mode to avoid using <ClientOnly>
#5394
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v4
Are you sure you want to change the base?
Conversation
| <slot name="fallback"> | ||
| <div class="size-8" /> | ||
| </slot> | ||
| <UButton |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The component is missing the ClientOnly wrapper and v-if="!colorMode?.forced" check that was present in the original code, which can cause the button to render when the color mode is forced and potentially cause SSR hydration mismatches.
View Details
📝 Patch Details
diff --git a/src/runtime/components/color-mode/ColorModeButton.vue b/src/runtime/components/color-mode/ColorModeButton.vue
index bcb2f8cd..e3cf1e47 100644
--- a/src/runtime/components/color-mode/ColorModeButton.vue
+++ b/src/runtime/components/color-mode/ColorModeButton.vue
@@ -27,9 +27,7 @@ const props = withDefaults(defineProps<ColorModeButtonProps>(), {
color: 'neutral',
variant: 'ghost'
})
-defineSlots<{
- fallback(props?: {}): any
-}>()
+
const { t } = useLocale()
const colorMode = useColorMode()
@@ -48,17 +46,23 @@ const isDark = computed({
</script>
<template>
- <UButton
- v-bind="{
- ...buttonProps,
- 'aria-label': isDark ? t('colorMode.switchToLight') : t('colorMode.switchToDark'),
- ...$attrs
- }"
- @click="isDark = !isDark"
- >
- <template #leading>
- <UIcon class="hidden dark:inline" :name="appConfig.ui.icons.dark" />
- <UIcon class="inline dark:hidden" :name="appConfig.ui.icons.light" />
+ <ClientOnly v-if="!colorMode?.forced">
+ <UButton
+ v-bind="{
+ ...buttonProps,
+ 'aria-label': isDark ? t('colorMode.switchToLight') : t('colorMode.switchToDark'),
+ ...$attrs
+ }"
+ @click="isDark = !isDark"
+ >
+ <template #leading>
+ <UIcon class="hidden dark:inline" :name="appConfig.ui.icons.dark" />
+ <UIcon class="inline dark:hidden" :name="appConfig.ui.icons.light" />
+ </template>
+ </UButton>
+
+ <template #fallback>
+ <div class="size-8" />
</template>
- </UButton>
+ </ClientOnly>
</template>
Analysis
ColorModeButton missing ClientOnly wrapper and forced mode check
What fails: ColorModeButton.vue renders button even when colorMode.forced is true and lacks ClientOnly wrapper for SSR safety, unlike other color mode components
How to reproduce:
- Set color mode to forced in Nuxt config:
colorMode: { forced: 'dark' } - Use
<UColorModeButton />in template - Button renders but clicking it has no effect (user cannot change forced mode)
Result: Button renders when it shouldn't, inconsistent with ColorModeSelect.vue and ColorModeSwitch.vue behavior
Expected: Button should not render when colorMode.forced is true, per official documentation pattern and consistency with other color mode components that use <ClientOnly v-if="!colorMode?.forced">
commit: |
<ClientOnly><ClientOnly>
<ClientOnly><ClientOnly>
benjamincanac
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've checked in the preview and it's rendered super small 🤔
Also, you need to remove the defineSlots which is useless.
Could this be worth applying the same changes in the Vue ColodeModeButton as well?
|
cant imagine what issue size could be. Have removed unused code |
| <UIcon :class="ui.leadingIcon({ class: props.ui?.trailingIcon })" class="hidden dark:inline" :name="appConfig.ui.icons.dark" /> | ||
| <UIcon :class="ui.leadingIcon({ class: props.ui?.trailingIcon })" class="inline dark:hidden" :name="appConfig.ui.icons.light" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| <UIcon :class="ui.leadingIcon({ class: props.ui?.trailingIcon })" class="hidden dark:inline" :name="appConfig.ui.icons.dark" /> | |
| <UIcon :class="ui.leadingIcon({ class: props.ui?.trailingIcon })" class="inline dark:hidden" :name="appConfig.ui.icons.light" /> | |
| <UIcon :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" class="hidden dark:inline" :name="appConfig.ui.icons.dark" /> | |
| <UIcon :class="ui.leadingIcon({ class: props.ui?.leadingIcon })" class="inline dark:hidden" :name="appConfig.ui.icons.light" /> |
Using props.ui?.trailingIcon instead of props.ui?.leadingIcon when styling leading icons, which applies incorrect styling classes.
View Details
Analysis
ColorModeButton uses trailingIcon property for leadingIcon styling
What fails: ColorModeButton.vue uses props.ui?.trailingIcon instead of props.ui?.leadingIcon when styling leading icons, causing incorrect styling classes to be applied
How to reproduce: Pass custom ui.leadingIcon styles to ColorModeButton component - they will be ignored, and any ui.trailingIcon styles will incorrectly be applied to the leading icons instead
Result: Leading icon styles are not applied correctly. Custom leadingIcon styling is ignored while trailingIcon styling incorrectly affects leading icons.
Expected: Leading icons should use props.ui?.leadingIcon property, consistent with all other components in the codebase (Badge.vue, Button.vue, Input.vue, H2.vue, H3.vue, etc.)
Pattern evidence: All 20+ other components use ui.leadingIcon({ class: props.ui?.leadingIcon }) but ColorModeButton uses ui.leadingIcon({ class: props.ui?.trailingIcon })
This is just the first one to see if the team are happy with it - will do rest of buttons if you are 😄 thanks for your work
EDIT
Cant really think of a normal way to do
UColorModeSwitchandUColorModeSelectandUColorModeAvatarandUColorModeImageare already using the method used in this implem.🔗 Linked issue
❓ Type of change
📚 Description
📝 Checklist