Style Kivy Apps with Themes
Learn to style Kivy apps with themes in this hands-on tutorial. Discover how to create consistent, professional-looking UIs using Kivy's theming system, then apply your skills in a practical exercise. Perfect for developers building mobile UIs with Python.
Focus: style kivy apps with themes
You've built a functional Kivy app with buttons, labels, and layouts, but it still looks like a default OS window — grey, flat, and forgettable. Every button screams 'default', every screen feels disconnected, and users judge your app in the first five seconds. The fix isn't more code; it's theming. In this lesson, you'll learn how to style Kivy apps with themes so your UI looks polished, consistent, and professional — without rewriting your entire app. By the end, you'll have a reusable theming system that transforms any Kivy project from prototype to product.
The problem this lesson solves
When you first ship a Kivy app, the UI uses Kivy's default style: a light grey background, plain buttons, and labels with black text. It's functional but visually bland. Worse, every widget looks the same — there's no visual hierarchy, no brand identity, and no way to signal state (like a disabled button or an error field). Users interpret this as 'unfinished' or 'low quality,' even if your logic is solid.
The deeper problem: you're probably hardcoding colors and sizes directly in each widget's properties. This creates style duplication — change the primary color in one place and you'll have to hunt through every file to update it everywhere else. It's fragile, error-prone, and makes your app impossible to rebrand or add a dark mode later. Theming solves both visual consistency and maintenance.
Pro tip: Theming isn't just about aesthetics. Consistent visual cues (color, spacing, typography) reduce cognitive load and make your app easier to navigate. A well-themed app feels more trustworthy.
Core concept / mental model
Think of theming like a design system for your app. Instead of telling each button 'I want this exact shade of blue', you define a palette and a style rule once, then every button automatically follows that rule. If you later decide to switch from blue to green, you change one line — every widget updates instantly.
In Kivy, the mechanism for this is the KV language and the Canvas instructions (like Color and Rectangle). While you can set background_color or color properties on many widgets, full control — shadows, rounded corners, gradients — requires drawing with Canvas. Theming centralizes these drawing instructions in reusable custom widget classes or KV rules.
Here's the mental model:
- Tokens = your design variables (e.g.,
PRIMARY_COLOR,FONT_SIZE_LARGE). In Python, these are constants or values in a config file. - Rules = how a widget uses those tokens (e.g., 'All buttons have primary color background and white text'). In KV, this is a rule like
Button:under a custom class or a styling class. - Widgets = the actual UI elements that inherit those rules automatically.
By separating tokens from rules, you can change the entire look of your app by editing a single theme file.
How it works step by step
To style Kivy apps with themes, follow this logic sequence:
- Define your theme tokens — colors, fonts, spacing, corner radii as Python constants or a config dictionary. This is your single source of truth.
- Create custom widget classes that use these tokens in their
Canvasdrawing. You subclass built-in widgets (e.g.,Button) and override thedrawlogic, or you use KV rules withcanvas.before/canvas.afterto add background shapes. - Apply the theme globally — set the default font, window background, and assign your custom classes to every widget you use. You can do this in the
.kvfile with rules like#:importand custom class names, or in Python by overriding thebuildmethod. - Use the theme in layouts and screens — your reusable components (buttons, text fields, cards) now reference the tokens, so any change propagates automatically.
- Maintain and extend — add dark mode, different accent colors, or responsive sizing by updating tokens, not individual widgets.
Cause → effect: If you change the PRIMARY_COLOR token, every widget that uses it repaints on the next frame — no manual updates needed.
Hands-on walkthrough
Let's build a themed Kivy app step by step. First, create a simple theme module with tokens.
# theme.py
PRIMARY = '#4CAF50' # Material Green
SECONDARY = '#FF5722' # Deep Orange
BACKGROUND = '#F5F5F5' # Light Grey
TEXT_COLOR = '#212121' # Dark Grey
WHITE = '#FFFFFF'
FONT_SIZE = '16sp'
CORNER_RADIUS = 8
Next, create a custom ThemedButton class that draws a rounded rectangle background using these tokens.
# main.py
import kivy
kivy.require('2.1.0')
from kivy.app import App
from kivy.uix.button import Button
from kivy.graphics import Color, RoundedRectangle, Rectangle
from kivy.utils import get_color_from_hex
from theme import PRIMARY, TEXT_COLOR, WHITE, CORNER_RADIUS
class ThemedButton(Button):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background_color = (0, 0, 0, 0) # make default background transparent
self.color = get_color_from_hex(WHITE)
self.font_size = '16sp'
self.size_hint = (None, None)
self.size = (200, 50)
self.bind(pos=self.redraw, size=self.redraw)
self.redraw()
def redraw(self, *args):
self.canvas.before.clear()
with self.canvas.before:
Color(*get_color_from_hex(PRIMARY))
RoundedRectangle(pos=self.pos, size=self.size, radius=[CORNER_RADIUS])
Now build a simple app that uses this button.
# main.py (continued)
from kivy.uix.boxlayout import BoxLayout
class ThemedApp(App):
def build(self):
layout = BoxLayout(padding=20, spacing=10)
btn = ThemedButton(text='Click Me')
layout.add_widget(btn)
return layout
if __name__ == '__main__':
ThemedApp().run()
Expected output: A window with a light grey layout (default) and a green rounded button with white text. Clicking it does nothing yet, but the visual is clean.
Now, let's expand to a full themed screen with a background and text field. Add a ThemedBackground widget and a ThemedTextInput.
# main.py (extended)
from kivy.uix.textinput import TextInput
from kivy.uix.label import Label
class ThemedTextInput(TextInput):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background_color = get_color_from_hex(WHITE)
self.foreground_color = get_color_from_hex(TEXT_COLOR)
self.padding = (10, 10)
self.size_hint = (None, None)
self.size = (200, 40)
self.bind(pos=self.redraw, size=self.redraw)
self.redraw()
def redraw(self, *args):
self.canvas.before.clear()
with self.canvas.before:
Color(*get_color_from_hex(PRIMARY))
RoundedRectangle(pos=self.pos, size=self.size, radius=[CORNER_RADIUS])
# main.py (build method updated)
from kivy.uix.gridlayout import GridLayout
class ThemedApp(App):
def build(self):
root = GridLayout(cols=1, padding=30, spacing=20, background_color=get_color_from_hex(BACKGROUND))
# set window background
from kivy.core.window import Window
Window.clearcolor = get_color_from_hex(BACKGROUND)
header = Label(text='Themed App', font_size='24sp', color=get_color_from_hex(TEXT_COLOR))
input_field = ThemedTextInput(hint_text='Name')
submit_btn = ThemedButton(text='Submit')
root.add_widget(header)
root.add_widget(input_field)
root.add_widget(submit_btn)
return root
Expected output: A clean screen with a white input field, a green button, and a dark header. The whole UI now follows the tokens you defined.
Pro tip: Use
canvas.beforefor the background so your text and icons stay on top. Usecanvas.afterfor overlays or shadows.
Compare options / when to choose what
When styling Kivy apps with themes, you have several approaches. Here's a comparison:
| Approach | Description | Best for | Pros | Cons |
|---|---|---|---|---|
| KV Language with custom styling classes | Use <-ClassName@Button> in .kv to inherit and override properties |
Simple apps, quick prototyping | Easy to read, declarative, no Python canvas code | Limited to built-in properties; advanced shapes require canvas anyway |
| Python custom widgets with Canvas | Subclass widgets and draw shapes in __init__ |
Full control, unique shapes, reusable components | Maximum flexibility, can animate, centralized logic | More code, requires manual redraw on size/pos changes |
| Third-party theme libraries (e.g., KivyMD) | Use Material Design components out-of-box | Material Design apps, fast development | Instant modern look, theming built-in | Adds dependency, limited customization |
When to choose what: - If you're building a simple app and just need a few color tweaks, use KV rules — minimal code. - If you want reusable, custom-styled components with rounded corners or gradients, use Python custom widgets. - If you're targeting Android/iOS and want a polished Material look with minimal effort, consider KivyMD — but understand its theming system separately.
Troubleshooting & edge cases
- Widget background not updating when I change position/size —
Canvasinstructions are tied to the widget'sposandsizeat draw time. If you don't bind toposandsize, the rectangle stays at the old location. Always useself.bind(pos=self.redraw, size=self.redraw)and clear/redraw in the callback. - Colors look washed out or transparent — Kivy's
Colorexpects values in 0–1 range, butget_color_from_hexreturns that correctly. If you hardcode(255, 0, 0), you'll get almost transparent red. Always useget_color_from_hexor divide by 255. - Text is invisible on a colored button — If you set
background_colorto a solid color, the default button text may be drawn behind the new background. Setself.colorto a contrasting color andself.background_color = (0,0,0,0)to avoid layers. - Custom widget doesn't show the theme when used inside a layout — Layouts may set
posandsizeautomatically. Ensure yourredrawmethod is bound and that you call it after the widget is added to the layout (or useon_size). - KivyMD conflicts with your custom theme — If you mix KivyMD and custom widgets, the order of canvas instructions matters. Use
canvas.beforewith a low alpha or restructure your UI to use one approach consistently.
What you learned & what's next
You've mastered the core idea behind styling Kivy apps with themes: separating style tokens from widget logic, then using custom widgets to apply those tokens consistently. You completed a hands-on exercise that turned a default grey app into a brand-consistent UI with themed buttons, input fields, and backgrounds. You can now explain how theming works, why it improves maintainability, and how to choose between KV rules, Python canvas code, and KivyMD.
In the next lesson, you'll learn how to add dark mode support with dynamic theming — switching your app's palette on the fly based on user preference. You'll build on the exact token system you created here, so keep your theme.py handy!
Now that you can style Kivy apps with themes, try applying a color scheme that reflects your own app's brand or a dark mode toggle. The power of a centralized theme will save you hours of UI polish down the line.
Practice recap
Create a new Kivy app and define a theme with two accent colors. Build a simple login screen using custom themed widgets (a text input and a button) and apply the theme. Change the primary color in your theme file and observe how the entire UI updates instantly — this confirms your theming system works. Optionally, add a second button with the secondary color to practice reusing your widgets.
Common mistakes
- Hardcoding colors directly in each widget instead of using theme tokens — this creates duplication and makes rebranding a nightmare.
- Forgetting to bind
posandsizewhen drawingCanvasshapes, causing backgrounds to stay stuck at the original position or size. - Using RGB values as floats between 0 and 1 but not converting hex colors with
get_color_from_hex, leading to washed-out or transparent appearances. - Setting
background_coloron a Button to a solid color but leaving the default text color black, making text invisible on dark backgrounds. - Mixing KivyMD and custom themed widgets without understanding canvas z-order, causing flickering or overlap.
Variations
- Use KV language rules like
<-ThemedButton@Button>to inherit and override properties directly in .kv files, reducing Python code. - Leverage KivyMD's built-in theming system to get Material Design styling with pre-made components and color palettes.
- Implement dynamic theme switching (light/dark) by binding theme tokens to app-level properties and updating them at runtime.
Real-world use cases
- A habit tracker app that uses a consistent brand color scheme across all screens, making it instantly recognizable in app stores.
- An internal company dashboard where primary actions are always the same accent color, reducing user error and training time.
- A white-label product that lets each client choose a custom theme via a config file, without changing any UI code.
Key takeaways
- Theming is a design system, not a one-off style: centralize colors, fonts, and spacings as tokens.
- Use custom widget classes with Canvas instructions to create rounded corners, gradients, and shadows that standard properties can't do.
- Bind
posandsizeto a redraw method so canvas shapes follow the widget's layout. - Always convert hex colors with
get_color_from_hexto ensure correct RGB values. - Choose between KV rules (simplicity), Python canvas (flexibility), and KivyMD (prebuilt Material) based on your app's needs.
- A centralized theme makes mobile app theming maintainable and easy to extend to dark mode or rebranding.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.