Skip to content

Conversation

363045841
Copy link

@363045841 363045841 commented Sep 23, 2025

Description

This PR introduces a new injection hook before-appearance-switch in the theme context.
It allows theme developers to define custom logic (synchronous or asynchronous) that will be executed before switching between dark and light appearance.
If the injected function returns false, the theme switch will be canceled.
Typical use-cases include custom animations, confirmation dialogs, or delaying the switch for UI synchronization.

For example, if I want to implement a background image switch animation after I click the button—which uses CSS that depends on the presence of class="dark"—the class is toggled immediately, so the transition does not have a chance to animate. The browser applies the new class and final styles in the same rendering frame, which causes the background to switch instantly without any smooth animation.

With this new before-appearance-switch hook, I can delay the actual theme class change until my custom animation or transition has completed. For instance, I can trigger an animation, wait for it to finish (using a Promise or timeout), and then toggle the .dark class, ensuring that the background image transition is smooth and visually appealing.

Example usage:

app.provide('before-appearance-switch', async () => {
  // Start animation (e.g. fade out old background)
  document.body.classList.add('theme-switching');
  await new Promise(resolve => setTimeout(resolve, 500)); // Wait for animation to finish
  document.body.classList.remove('theme-switching');
  return true; // Proceed with the appearance switch
});

Why is this needed?
Because toggling the .dark class immediately does not allow transitions/animations that depend on the class change to play smoothly. This hook gives developers precise control over when the class is toggled, so they can coordinate it with custom UI effects.

Hook Usage example:

// In enhanceApp:
app.provide('before-appearance-switch', async () => {
  // e.g. play animation or wait for an async operation
  await new Promise(resolve => setTimeout(resolve, 800))
  return true
})

In your theme component:

const beforeThemeSwitch = inject('before-appearance-switch', () => true)

const handleClick = async () => {
  const result = await beforeThemeSwitch()
  if (result !== false) {
    toggleAppearance()
  }
}

This change is fully backward compatible:
If before-appearance-switch is not provided, the theme switch works as before.


Linked Issues

No related issues found.


Additional Context

  • The hook supports both synchronous and asynchronous functions.
  • If the injected function throws an error, the appearance switch will be aborted.
  • This feature enables more flexible UI/UX and advanced customization for theme switching.

Tip

The author of this PR can publish a preview release by commenting /publish below.

@brc-dd
Copy link
Member

brc-dd commented Sep 23, 2025

If you just need to wait for some time, you can write something like this: 👀

// .vitpress/config.ts

import { defineConfig } from 'vitepress'

export default defineConfig({
  appearance: {
    onChanged(isDark, defaultHandler, mode) {
      document.body.classList.add('theme-switching')
      setTimeout(() => {
        document.body.classList.remove('theme-switching')
        defaultHandler(mode)
      }, 500)
    }
  }
})

The function must be pure for serialization to work.

Because toggling the .dark class immediately does not allow transitions/animations that depend on the class change to play smoothly.

You can set appearance: { disableTransition: false } if you want to use CSS animations. But it doesn't provide an intermediate state like what you wrote in your example. Also, things might appear wonky if you're using the default theme.

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

Successfully merging this pull request may close these issues.

2 participants