Progress bar
A progress bar is a visual element that can be used to display, in a more playful way, the number of hit points, mana, or any other resource of your character sheet.
There are several methods to build a progress bar. In this page, we’ll cover four different approaches, each with its own advantages and disadvantages.
Dynamic Progress Bar
Let’s start with the simplest method. The dynamic progress bar makes it easy to manage large ranges of values and is highly customizable, since its appearance can be modified in just a few clicks.
With some coding skill, you can even make progress bars that players themselves can personalize.
The bar works in a very simple way:
- The filled portion of the bar is displayed using one symbol (for example, a ga_plain-square).
- The empty portion is displayed using another symbol (for example a ga_square or a space " ").
⚠️ The drawback is that this bar is not interactive. To change its value, you’ll need to add [+] and [–] buttons or directly edit the NumberInput components.

🔧 How to Build It
You need 3 components:
"bar1_current_value""bar1_max_value""bar1_progress"
init = function(sheet){
initProgressBar(sheet,"bar1_progress", "bar1_current_value",
"bar1_max_value", ":ga_plain-square:",
":ga_square:",10);
}
const initProgressBar = function(sheet,bar_id, current_value_id, max_value_id, fill_icon, empty_icon, line_length ){
let current = sheet.get(current_value_id);
let max = sheet.get(max_value_id);
let bar = sheet.get(bar_id);
const updateBar = function(){
let current_value = current.value();
let max_value = max.value();
let bar_value = "";
for(let i = 0; i < max_value; i++) {
if (i != 0 && i%line_length==0){
bar_value += "\n";
}
if ( i < current_value){
bar_value += fill_icon;
} else {
bar_value += empty_icon;
}
}
if (max_value < current_value){
current.value(max_value);
}
bar.value(bar_value);
}
current.on("update", updateBar);
max.on("update", updateBar);
}
✨ You can modify its appearance by:
- Changing the classes of
"bar1_progress"by changing the color or the size. - Adjusting the line length argument.
- Using any available icon in LetsRole (list1 liste2).
Two-Color Progress Bar
This progress bar is an upgrade of the first one: it differentiates the filled and empty portions with two different colors.

🔧 How to Build It
You’ll need 5 components:
"bar2_current_value""bar2_max_value"- Two Labels side by side →
"bar3_progress_middle_c1"and"bar3_progress_middle_c2"(these form the mixed line with the last filled + empty parts)
"bar3_progress_c1"- A Label below for fully empty lines →
"bar3_progress_c2"

init = function(sheet){
initProgressBarTwoColor(sheet, "bar2_progress_c1", "bar2_progress_middle_c1",
"bar2_progress_c2", "bar2_progress_middle_c2",
"bar2_current_value", "bar2_max_value",
":ga_plain-square:", ":ga_plain-square:",10);
}
const initProgressBarTwoColor = function(sheet,sub_bar_c1_id, bar_c1_id,
sub_bar_c2_id,bar_c2_id,
current_value_id, max_value_id,
fill_icon, empty_icon, line_length){
let current = sheet.get(current_value_id);
let max = sheet.get(max_value_id);
let sub_bar1 = sheet.get(sub_bar_c1_id);
let bar1 = sheet.get(bar_c1_id);
let sub_bar2 = sheet.get(sub_bar_c2_id);
let bar2 = sheet.get(bar_c2_id);
const stringMul = function(string, mul){
let out_string = "";
for(let i = 0 ; i < mul ; i++){
if(i != 0 && i % line_length == 0){
out_string += "\n";
}
out_string += string;
}
return out_string;
}
const updateBar = function(){
let current_value = current.value();
let max_value = max.value();
if (max_value < current_value){
current.value(max_value);
current_value = max_value;
}
let left = max_value - current_value;
let on_top_bar = current_value - (current_value % line_length);
let on_midle_bar1 = current_value % line_length;
let on_midle_bar2 = (left < line_length - on_midle_bar1) ? left : line_length - on_midle_bar1;
let on_bot_bar = max_value - current_value - on_midle_bar2;
sub_bar1.value(stringMul(fill_icon, on_top_bar));
bar1.value(stringMul(fill_icon, on_midle_bar1));
bar2.value(stringMul(empty_icon, on_midle_bar2));
sub_bar2.value(stringMul(empty_icon , on_bot_bar));
}
current.on("update", updateBar);
max.on("update", updateBar);
}
✨ Like the first version, it’s easily customizable — but with an extra color to highlight different resources.
Clickable Progress Bar
This progress bar is interactive: players can click on it to change its value.
⚠️ Drawbacks:
- More difficult and time-consuming to set up.
- The maximum value must be fixed when designing the sheet (not dynamic).
- Each icon can display a différent icon.
Here, the NumberInputs can be hidden so only the bar is visible.
🔧 How to Build It
You’ll need:
- A NumberInput for the current value →
"bar4_current_value" - A NumberInput for the maximum value →
"bar4_max_value" - A series of Labels or Icons equal to the max size whithe precise id name (e.g.,
bar4_0,bar4_1, ...,bar4_30, ...)
init = function(sheet){
initButtonProgressBar(sheet,"bar4_", 20, "bar4_current_value", "bar4_max_value", "text-success ", "text-danger opacity-50" , "d-none");
}
const initButtonProgressBar = function(sheet,bar_prefix, bar_nb_element, current_value_id, max_value_id, fill_class, empty_class, disable_class ){
let current= sheet.get(current_value_id);
let max = sheet.get(max_value_id);
let button_container = sheet.get(button_container_id);
let empty_class_list = empty_class.split(" ");
let fill_class_list = fill_class.split(" ");
const addClasses = function(cmp, class_liste){
for(let i = 0; i< class_liste.length; i++){
cmp.addClass(class_liste[i]);
}
}
const removeClasses = function(cmp, class_liste){
for(let i = 0; i< class_liste.length; i++){
cmp.removeClass(class_liste[i]);
}
}
const updateBarOnButton = function(progress){
let i = 0;
while (i < bar_nb_element){
let button_i = sheet.get(bar_prefix + i);
if ( i <= progress){
removeClasses(button_i, empty_class_list);
addClasses(button_i, fill_class_list);
} else {
removeClasses(button_i, fill_class_list);
addClasses(button_i, empty_class_list);
}
i++;
}
}
const updateMax = function(){
let i = 0;
let max_value = max.value()-1;
while (i < bar_nb_element){
let button_i = sheet.get(bar_prefix + i);
if ( i <= max_value){
button_i.removeClass(disable_class);
} else {
button_i.addClass(disable_class);
}
i++;
}
}
const barClicked = function(btn){
if(btn.id().includes(bar_prefix)){
let n = parseInt(btn.id().split(bar_prefix)[1]);
current.value(n);
updateBarOnButton(n);
}
}
button_container.on("click",".label",barClicked);
max.on("update",updateMax);
current.on("update",function(){updateBarOnButton(current.value()-1)});
updateBarOnButton(current.value()-1);
updateMax();
}
✨ Although harder to implement, this bar is very user-friendly since players can simply click on it.
Also, the buttons don’t need perfect formatting — you can arrange them however you like.
