Unofficial - Progress Bar

Alright, based on popular demand, I am happy to introduce… :drum::drum::drum:

Progress Bar for Softr Details Pages!

Live Demo:

How to Implement

Setup

  1. Add a Status field in Airtable (typically a dropdown)
  2. Create a field to store the “Status HTML”

Generating the HTML

  1. Method 1: Use a formula field (instant but long and complicated)
  2. Method 2: Use an automation that runs on status update (more control but slower for Airtable, instant in Xano)

Softr Setup

  1. On the details page, duplicate the item details block
  2. Delete everything, hide the media and add the Status HTML field as “Rich Text” to the first section so there is no label
  3. Add a custom code block to store the styling (CSS given below)
  4. Voila :tada:

Code

The code given below assumes a status bar with five statuses:

  1. Backlog
  2. Ready for Dev
  3. In Progress
  4. In Review
  5. Shipped.

Use AI to update for your use case.

Method 1 - Formula

"<div class='status-bar'>" &

"<div class='stage " &
IF({Status} = "Backlog", "active", IF(
  OR({Status} = "Ready for Dev", {Status} = "In Progress", {Status} = "In Review", {Status} = "Shipped"), "completed", "todo")) &
"'><div class='circle'><span>" &
IF(OR({Status} = "Ready for Dev", {Status} = "In Progress", {Status} = "In Review", {Status} = "Shipped"), "&#10003;", "1") &
"</span></div><div class='label'>Backlog</div></div>" &

"<div class='stage " &
IF({Status} = "Ready for Dev", "active", IF(
  OR({Status} = "In Progress", {Status} = "In Review", {Status} = "Shipped"), "completed", "todo")) &
"'><div class='circle'><span>" &
IF(OR({Status} = "In Progress", {Status} = "In Review", {Status} = "Shipped"), "&#10003;", "2") &
"</span></div><div class='label'>Ready for Dev</div></div>" &

"<div class='stage " &
IF({Status} = "In Progress", "active", IF(
  OR({Status} = "In Review", {Status} = "Shipped"), "completed", "todo")) &
"'><div class='circle'><span>" &
IF(OR({Status} = "In Review", {Status} = "Shipped"), "&#10003;", "3") &
"</span></div><div class='label'>In Progress</div></div>" &

"<div class='stage " &
IF({Status} = "In Review", "active", IF(
  {Status} = "Shipped", "completed", "todo")) &
"'><div class='circle'><span>" &
IF({Status} = "Shipped", "&#10003;", "4") &
"</span></div><div class='label'>In Review</div></div>" &

"<div class='stage " &
IF({Status} = "Shipped", "active", "todo") &
"'><div class='circle'><span>" &
IF({Status} = "Shipped", "5", "5") &  
"</span></div><div class='label'>Shipped</div></div>" &

"</div>"

Method 2 - Automation

let { status } = input.config();

function generateStatusHTML(status) {
  const trackerStages = [
    'Backlog',
    'Ready for Dev',
    'In Progress',
    'In Review',
    'Shipped'
  ];

  const currentIndex = trackerStages.indexOf(status);
  
  function getStageClass(index) {
    if (index < currentIndex) return 'completed';
    if (index === currentIndex) return 'active';
    return 'todo';
  }

  function getIconForClass(stageClass, index) {
    return stageClass === 'completed' ? '&#10003;' : index + 1;
  }

  let html = '<div class="status-bar">';

  trackerStages.forEach((stage, index) => {
    const stageClass = getStageClass(index);
    const icon = getIconForClass(stageClass, index);

    html += `
      <div class="stage ${stageClass}">
        <div class="circle"><span>${icon}</span></div>
        <div class="label">${stage}</div>
      </div>
    `;
  });

  html += '</div>';
  return html.replace(/\s{2,}|\n/g, '');
}

output.set('statusHTML', generateStatusHTML(status));

CSS (Add to Softr)

Make sure the ID on the first style matches the ID of the block where you want to render the Status Bar.

<style>
  #list-details1 section {
    justify-content: center;
  }

  .status-bar {
    display: flex;
    justify-content: space-around;
    align-items: center;
    max-width: 800px;
    margin: 0 auto;
    position: relative;
  }

  .status-bar::before {
    content: "";
    position: absolute;
    top: 24px;
    left: 5%;
    right: 5%;
    height: 4px;
    background-color: #e0f1ff; /* Light blue to complement primary */
    z-index: 0;
  }

  .stage {
    text-align: center;
    position: relative;
    z-index: 1;
    width: 100%;
  }

  .stage:not(:last-child)::after {
    content: "";
    position: absolute;
    top: 24px;
    left: 50%;
    width: 100%;
    height: 4px;
    background: #3278FF; /* Solid primary color line */
    z-index: -1;
  }

  .circle {
    width: 48px;
    height: 48px;
    border: 4px solid #3278FF; /* Primary Blue */
    border-radius: 50%;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: bold;
    font-size: 18px;
    color: #fff;
    background-color: #3278FF; /* Primary Blue */
    margin: 0 auto 10px;
  }

  .completed .circle {
    background-color: #3278FF;
    color: #fff;
  }

  .todo .circle {
    background-color: #fff;
    color: #3278FF;
    border: 4px solid #3278FF;
  }

  .active .circle {
    background-color: #fff;
    color: #3278FF;
    border: 4px solid #3278FF;
    outline: 4px solid rgba(50, 120, 255, 0.4); /* Soft glow */
    animation: pulse 1s infinite cubic-bezier(0.25, 0.1, 0.25, 1) 3s;
  }

  .label {
    font-size: 14px;
    color: #3278FF;
    padding: 0 25px;
    height: 15px;
  }

  @keyframes pulse {
    0% {
      transform: scale(1);
      box-shadow: 0 0 0 0 rgba(50, 120, 255, 0.6);
    }
    70% {
      transform: scale(1.1);
      box-shadow: 0 0 0 10px rgba(50, 120, 255, 0);
    }
    100% {
      transform: scale(1);
      box-shadow: 0 0 0 0 rgba(50, 120, 255, 0);
    }
  }
  
  @media (max-width: 600px) {
    .status-bar {
      gap: 6px;
    }

    .circle {
      width: 28px;
      height: 28px;
      font-size: 12px;
      margin-bottom: 2px;
    }

    .label {
      display: none;
    }

    .stage {
      min-width: 40px;
    }
    
    .status-bar::before,
    .stage:not(:last-child)::after {
      height: 2px;
      top: 14px;
    }
  }
</style>

That’s it! Hope it’s useful!

7 Likes

Thank you for sharing

1 Like

You bet!

Great! to be clear, if I want to use method 2, I create a text field “Status HTML” ( I already have the “Status” drop-down list) then an automation that is triggered with the change in status and I integrate the script of method 2 ( with the update of my status data)?

The script returns HTML, and you need an additional step to store it in the HTML field.

1 Like

I just modified the script to do it automatically

Thank you for your answer.

It works! is it normal that on mobile format, the text does not appear?

1 Like

Great! Yeah, the labels were taking too much space on mobile, but you can always unhide them within the CSS.

I replaced “none” with “flex”: label {
display: flex;

It’s perfect!

I will use for user avencement for my accompaniment but I have about 20 steps, can be put on several lines

1 Like

20 steps is a lot! Maybe you should think about breaking it up into categories so each is a max of 5 steps? Anyways, happy Sahil provide a solution!!

@Jjenglert see this picture (


)

I find it interesting that the client sees his progress on the full accompaniment as an itinerary.

on the mobile version, I have a scroller bar ( perfect) but on tablet and computer version, it is placed at the bottom of the screen ( and not on the lower part of the block)

Thank you Sahil! thanks to you, I can offer a better user experience.

Next time, I will test it for new customer onboarding

1 Like

Very cool!!! Well done :+1:t2: