Skip to content

Conversation

JustinHaut
Copy link

📜 Description

Fixed Android KeyboardToolbar dark mode detection by creating a new useKeyboardToolbarTheme hook that implements a fallback strategy when keyboard appearance detection is unreliable. The hook falls back to system appearance when keyboard reports "light" but the system is actually in dark mode.

💡 Motivation and Context

On Android devices, the KeyboardToolbar components (toolbar, buttons, arrows) incorrectly display in light mode even when the system is in dark mode. This happens because keyboard appearance detection from native events can be unreliable across different Android devices and keyboard implementations.

Evidence from Galaxy S22 (API 35):

🔍 Debug - Keyboard appearance: light // ❌ Incorrect
🔍 Debug - System appearance: dark // ✅ Correct

This issue affects modern Android devices due to OEM customizations and keyboard implementation differences, not just older API versions.

📢 Changelog

JS

  • Added useKeyboardToolbarTheme hook with system appearance fallback logic
  • Centralized theme detection logic to eliminate code duplication
  • Made hook extensible for future customization features

iOS

  • No changes (iOS keyboard appearance detection works correctly)

Android

  • Fixed KeyboardToolbar showing light theme when system is in dark mode
  • Improved dark mode compatibility across different Android devices and OEMs

🤔 How Has This Been Tested?

  • Build Testing: Successfully builds with yarn prepare
  • Type Safety: Passes TypeScript checks with yarn typescript
  • Code Quality: Passes linting with yarn lint
  • Android Device Testing: Tested on Galaxy S22 (API 35) running Android in dark mode
  • iOS Device Testing: Tested on iPhone 14 Pro - dark mode continues to work correctly
  • Issue Confirmation: Verified keyboard appearance incorrectly reports "light" while system appearance correctly reports "dark" on Android
  • No Regressions: iOS keyboard appearance detection remains accurate (no fallback needed)
  • Architecture: Follows existing hook directory structure pattern (useKeyboardState/, useWindowDimensions/)

Testing Environment:

  • Android: Samsung Galaxy S22, API 35 (Android 15), Dark mode enabled
  • iOS: iPhone 14 Pro, Dark mode enabled
  • Keyboard: Default system keyboards on both platforms

Results:

  • Android: ✅ Fixed - KeyboardToolbar now shows correct dark theme
  • iOS: ✅ No changes - KeyboardToolbar continues to work correctly

📸 Screenshots:

Before:

6484819227352374807

After:

4589679093113398099

📝 Checklist

  • CI successfully passed (will be verified by GitHub Actions)
  • I added new mocks and corresponding unit-tests if library API was changed (No API changes - internal hook only)

@kirillzyusko kirillzyusko self-requested a review September 11, 2025 08:38
@kirillzyusko kirillzyusko self-assigned this Sep 11, 2025
@kirillzyusko kirillzyusko added 🐛 bug Something isn't working KeyboardToolbar Anything related to KeyboardToolbar component labels Sep 11, 2025
@kirillzyusko
Copy link
Owner

kirillzyusko commented Sep 11, 2025

Hey @JustinHaut

Thank you for looking into this and than you for your PR! I appreciate that! ❤️

I like the idea of the fix, but I strongly believe that it should be handled in:

fun Context.isSystemDarkMode(): Boolean =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
(getSystemService(Context.UI_MODE_SERVICE) as? android.app.UiModeManager)
?.nightMode == android.app.UiModeManager.MODE_NIGHT_YES
} else {
false
}

It seems like this method returns false when it should return true? The reason why I want to have a fix in a native code is because:

  • I'd like to use useKeyboardState(state => state.appearance) code because I use it already in many places (in example app);
  • isSystemDarkMode is also used to configure KeyboardBackgroundView on a native side;

So maybe you can see how react-native detects theirs colorScheme in native code and we can replicate a fix in native code? I'd like to use isSystemDarkMode as a single source of truth and don't change existing JS code 🙂

P. S. it looks like react-native uses this code:

/** Utility object providing static methods for working with UI mode properties from Context. */
internal object UiModeUtils {

  /**
   * Determines whether the current UI mode is dark mode
   *
   * @param context The context to check the UI mode from
   * @return true if the current UI mode is dark mode, false otherwise
   */
  @JvmStatic
  fun isDarkMode(context: Context): Boolean =
      context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
          Configuration.UI_MODE_NIGHT_YES
}

Worth to check the result of my implementation and this function 👀

Also I'm not sure if my implementation is sensitive to Appearance.setColorScheme() - I believe not. But I don't remember exactly - does this method override keyboard background color? I think not on Android (on iOS it does, but not on Android).

Also - are you sure that you use dark theme across the whole OS? Maybe you just customized keyboard appearance and OS itself is running in light theme? And in app you force dark theme via Appearance.setColorScheme()? Curious to hear more about all settings that you use in your app 😊

@JustinHaut
Copy link
Author

Hi @kirillzyusko

Thank you for the awesome library and for your guidance on the native fix! I've confirmed this works on system theme changes.

fun Context.isSystemDarkMode(): Boolean = 
  resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES

-You're correct, implementation is not sensitive to Appearance.setColorScheme() changes. Looks like neither iOS nor Android follow the setColorScheme()
-Yes, while developing I change theme at system level through the device's pulldown menu quick action.
-App allows users to override the system theme via Appearance.setColorScheme() and choose their preferred theme which gets applied at launch.

So the native fix now correctly handles system detection, but none of the components reflect app-level overrides from Appearance.setColorScheme().

In the meantime, would you like me to adjust the PR to reflect the Android native fix only?

...thinking about it - if users prefer dark mode, most probably keep their system set on it at the system level anyway 🤔

@kirillzyusko
Copy link
Owner

App allows users to override the system theme via Appearance.setColorScheme() and choose their preferred theme which gets applied at launch.

Again, from what I remember using Appearance.setColorScheme("dark") will make keyboard dark on iOS, but on Android it'll be ignored and keyboard will have a color that is managed by OS preferences.

In the meantime, would you like me to adjust the PR to reflect the Android native fix only?

Yes, please do 🙂 The bug that you describe is Android only anyway, so fix for Android only (in native code) looks good for me 😊 Once you do it I'll run e2e tests and will test PR manually too to confirm that it doesn't break anything!

Thank you for your PR again 🙌

- Use Configuration.UI_MODE_NIGHT_MASK for reliable system dark mode detection
- Remove JavaScript fallback approach for cleaner native-only solution
- Fix issue where toolbar showed light theme when system was in dark mode

Tested on Galaxy S22 (API 35) - now correctly detects dark mode
@JustinHaut
Copy link
Author

Thanks for the feedback! Updated the PR to use the native-only approach. The JavaScript fallback has been removed and now it's just the isSystemDarkMode() function using Configuration.UI_MODE_NIGHT_MASK. 🫡

@kirillzyusko
Copy link
Owner

Thanks! I'll test implementation over weekend and will merge if don't find any new bugs 🙂
In a meantime I approved and run workflows to see that new code doesn't break CI 🙂

Thank you again for your contribution!

Copy link
Contributor

📊 Package size report

Current size Target Size Difference
212957 bytes 212978 bytes -21 bytes 📉

@kirillzyusko
Copy link
Owner

Hey @JustinHaut

I tested this implementation more, and as I was suspecting before - the new implementation is sensitive to Appearance.setColorScheme() call, but system UI is not sensitive to it, so you may get an incorrect result here:

image

In this example (tested on API 35 emulator) project I used non-dark theme on OS level, but used Appearance.setColorScheme("dark") in App.tsx. As a result isSystemDarkMode returns true, but keyboard is in light mode in reality...

@JustinHaut
Copy link
Author

Just noticed that YouTube has a light keyboard with system light and app dark too, so by that standard this seems reasonable 🙃 Thank you!! 🙂

@kirillzyusko
Copy link
Owner

@JustinHaut so what should we do here? Will you close the PR (since it'll bring new bugs too)?

I'll try to use Samsung device and play around dark theme to confirm that Samsung devices really return wrong dark theme detection 🤔

@JustinHaut
Copy link
Author

Seems like this is where the javascript tweak would need to come in to achieve true parity.

I personally don't think the dark bar when respecting system mode but not in-app theme looks bad. The white bar on dark background is more jarring IMO.

But it's totally up to you. I can close it if you'd prefer revisit this at another time.

@kirillzyusko
Copy link
Owner

Seems like this is where the javascript tweak would need to come in to achieve true parity.

In your JS code yo wrote:

if (keyboardColorScheme === "light" && systemColorScheme === "dark") {
    return "dark";
  }

Which is literally: if myOldCodeDetectLight && Appearance.colorSchemeSetToDark, but I published a screenshot #1116 (comment) that shows that on Pixel emulator if system is in light mode and Appearance forced to be dark, then keyboard still in light mode... So that condition in JS is also not very correct, I guess?

I think we need to understand why system dark mode is not recognized by my current and how we can make it recognizable 🤔 This is really sad that Android doesn't have an API to query the keyboard theme and we need to use transitive dependencies to "guess" the color 😔

This PR aims to solve one problem, but adds new bugs, so I can not merge it as is. I'll try to run Samsung device to see why dark theme can not be detected by my native code!

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

Labels

🐛 bug Something isn't working KeyboardToolbar Anything related to KeyboardToolbar component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants