2025, Nov 16 09:00

Kivy .kv styles ignored for prebuilt/injected widgets: understanding load order and applying the correct fix

Injected Kivy widgets ignore .kv styles if created before build(). See why timing breaks class rules and fix it via late creation or early .kv load. Now.

When you inject a prebuilt widget into a Kivy app, it’s easy to hit a subtle trap: the injected instance ignores the .kv styling and looks “raw”, while widgets created later have the expected appearance. The behavior is confusing until you remember when Kivy actually loads the .kv rules.

Problem setup

Consider a layout described in a .kv file and a custom widget instantiated ahead of time and passed into the app as a dependency. The injected widget renders without the styles declared in the .kv.

# minimal_example/sample.kv
<MainShell>
    BrandedButton:
        text: 'Custom Button'

<BrandedButton>
    text: 'This is a custom button'
    background_color: (0.5, 0.5, 0.5, 1)
    font_size: 25
# minimal_example/custom_widgets.py
from kivy.uix.button import Button


class BrandedButton(Button):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

WIDGET_SINGLETON = BrandedButton(text='Constant')
# minimal_example/app_with_kv.py

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

from minimal_example.custom_widgets import BrandedButton, WIDGET_SINGLETON


class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))  # Styled correctly
        self.add_widget(injected_button)  # Missing styles


class SampleApp(App):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self._injected_button = injected_button

    def build(self):
        return MainShell(self._injected_button)


if __name__ == '__main__':
    SampleApp(WIDGET_SINGLETON).run()

What’s really going on

The mismatch comes from timing. The instance stored in WIDGET_SINGLETON is constructed in custom_widgets.py before the .kv rules are loaded. Kivy’s App automatically loads a correctly named .kv file for the App class, and that happens inside build(). Any widget created before that moment won’t get the class rules from the .kv file, so the injected button misses its styling. Widgets created after build() (for example, the dynamically added one) do get the rules and look as expected.

How to fix it

There are two straightforward options. The first is to stop creating the widget before the .kv is loaded and instead create it inside build(), after the .kv has been applied. This shifts where the instance is built.

# minimal_example/custom_widgets.py
from kivy.uix.button import Button


class BrandedButton(Button):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

# Note: no module-level instance here
# minimal_example/app_with_kv_fixed_build.py
from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

from minimal_example.custom_widgets import BrandedButton


class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))
        self.add_widget(injected_button)


class SampleApp(App):
    def build(self):
        late_instance = BrandedButton(text='Constant')
        return MainShell(late_instance)


if __name__ == '__main__':
    SampleApp().run()

If you need to preserve the dependency injection shape and keep constructing the instance outside the app, the second option is to load the .kv file earlier, before the import that creates the widget. To avoid loading the same .kv twice, rename the .kv file to a different name and explicitly load it. For example, rename sample.kv to sample_manual.kv and ensure it is loaded before custom_widgets.py is imported.

# minimal_example/app_with_kv_early_load.py
from kivy.lang import Builder

# Load the renamed kv file early
Builder.load_file('minimal_example/sample_manual.kv')

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout

# Import happens after kv is loaded
from minimal_example.custom_widgets import BrandedButton, WIDGET_SINGLETON


class MainShell(BoxLayout):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self.add_widget(BrandedButton(text='Added dynamically'))
        self.add_widget(injected_button)


class SampleApp(App):
    def __init__(self, injected_button: BrandedButton, **kwargs):
        super().__init__(**kwargs)
        self._injected_button = injected_button

    def build(self):
        return MainShell(self._injected_button)


if __name__ == '__main__':
    SampleApp(WIDGET_SINGLETON).run()

This way the .kv rules are in place before the instance is created, and the injected widget renders with the expected layout. The renaming prevents the App from auto-loading the file a second time.

Why this matters

In Kivy, the order of operations dictates whether .kv class rules are applied to widget instances. Any code that constructs widgets at module import time can silently bypass those rules if the .kv hasn’t been loaded yet. That’s particularly relevant when you apply patterns like dependency injection or maintain module-level singletons.

Takeaways

Keep widget instantiation on the safe side of the .kv load boundary. If you can, create instances in build(), after the App has loaded its .kv. If you do need early instantiation, load the .kv first and rename it to avoid duplicate loading. Following this sequence prevents subtle layout inconsistencies and keeps your widget styling predictable.