This feature is not documented at all in LVGL, and many developers would love to know about that! That's why this post exists!
a.k.a Real inheritance in C
Most developers know LVGL for its lightweight rendering engine and friendly widget API, but behind the scenes is a surprisingly powerful class system that mimics object-oriented inheritance in pure C.
In this post, we’ll explore how to extend an existing widget, such as a button, and inject your own custom behavior. (for example, a button_with_fade_in_anim). You’ll learn how to build on top of lv_btn using LVGL’s internal lv_obj_class_t system.
What is lv_obj_class_t
In LVGL 8.x and newer, every object is associated with an object class defined by lv_obj_class_t. This class structure defines:
A base class (optional)
A constructor
A destructor
An event callback
The size of the extended user data (like subclassing fields)
Think of it as a C version of a vtable-based class in C++. This system allows you to create your own widget types while still leveraging LVGL’s internal rendering and event engine.
Goal: Create a Custom Button with a Fade-In Animation
Let’s extend the built-in lv_btn class to make a button that fades in when it appears.
Includes in LVGL 9.x
The API is no longer directly exposed to the user by default. You'll need to explicitly include the following:
#include "core/lv_obj.h"
#include "core/lv_obj_class_private.h"
#include "core/lv_obj_private.h"
Define the custom structure
We want to create a custom button that supports a fade-in animation, and we want to expose a method like button_with_fade_set_fade_in_time().
struct button_with_fade_in {
lv_obj_t obj; /* Required base */
uint32_t fade_in_time; /* Custom property */
};
Define the class and Constructor
We create the class and register a constructor to initialize it.
static void button_with_fade_in_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj);
const lv_obj_class_t button_with_fade_in_class = {
.constructor_cb = button_with_fade_in_constructor,
.instance_size = sizeof(struct button_with_fade_in),
.base_class = &lv_btn_class
};
lv_obj_t * button_with_fade_in_create(lv_obj_t * parent) {
lv_obj_t * obj = lv_obj_class_create_obj(&button_with_fade_in_class, parent);
lv_obj_class_init_obj(obj);
return obj;
}
The constructor can set defaults or add extra styling:
static void button_with_fade_in_constructor(const lv_obj_class_t * class_p, lv_obj_t * obj) {
LV_UNUSED(class_p);
struct button_with_fade_in * self = (struct button_with_fade_in *)obj;
/* Default fade-in time */
self->fade_in_time = 300;
/* Optional default behavior (e.g., style) */
lv_obj_set_style_bg_color(obj, lv_palette_main(LV_PALETTE_BLUE), 0);
}
This object can be used like any lv_btn_t, so like any lv_obj_t, as everything inherits from this base class. That is really powerful right?
Define more... if you need
This class system lets you override:
Constructor: Custom initialization
Destructor: Free resources
Event handler: Hook into LVGL’s internal event stream (like LV_EVENT_DRAW_PART_BEGIN, LV_EVENT_CLICKED, etc.)
Example:
static void my_event_cb(lv_event_t * e) {
if(lv_event_get_code(e) == LV_EVENT_CLICKED) {
LV_LOG_USER("Custom click behavior!");
}
lv_obj_event_base(e); // Call base class handler
}
const lv_obj_class_t button_with_fade_in_class = {
.constructor = button_with_fade_in_constructor,
.event_cb = my_event_cb,
.base_class = &lv_btn_class,
.instance_size = sizeof(struct button_with_fade_in)
};
Add a "Method" Function
Here's where it all comes together. Just define a function that receives the lv_obj_t * returned by button_with_fade_in_create, casts it to your class, and updates your field.
void button_with_fade_set_fade_in_time(lv_obj_t * obj, uint32_t ms) {
struct button_with_fade_in * self = (struct button_with_fade_in *)obj;
self->fade_in_time = ms;
}
You can also trigger the animation directly:
void button_with_fade_run_fade_in(lv_obj_t * obj) {
struct button_with_fade_in * self = (struct button_with_fade_in *)obj;
lv_anim_t a;
lv_anim_init(&a);
lv_anim_set_var(&a, obj);
lv_anim_set_values(&a, LV_OPA_TRANSP, LV_OPA_COVER);
lv_anim_set_time(&a, self->fade_in_time);
lv_anim_set_exec_cb(&a, (lv_anim_exec_xcb_t)lv_obj_set_style_opa);
lv_anim_start(&a);
}
The only real limit to customizing your widget is your imagination!
Well... and maybe the poor little device you're running it on, it has feelings too, you know 😉
Usage
All of this comes together to make it super easy to use... which was exactly the goal from the start!
lv_obj_t * btn = button_with_fade_in_create(lv_scr_act());
button_with_fade_set_fade_in_time(btn, 500);
button_with_fade_run_fade_in(btn);
Final thoughts
LVGL’s class system might feel hidden beneath its beginner-friendly surface, but it’s a powerful tool for developers building scalable, maintainable, and customized widgets.
If you’re building a serious UI, especially for embedded Linux, it’s worth learning how to extend widgets with this system. You’ll gain performance, reusability, and cleaner architecture, all within the safety of static typing and zero dynamic allocation.