Enhance UX in Flow with Custom LWC Icon Selector

When designing user flows in Salesforce, providing a seamless and visually appealing experience can make a big difference in user engagement. While using the standard Flow elements to build an interface may suffice, it often results in a basic design that falls short of delivering a polished experience. To better the user experience, we can opt to create a lightweight Lightning Web Component (LWC) to replace the default button group. The result? A modernized flow screen that’s visually engaging and user-friendly.
Now, let’s explore how you can integrate this component into your flows, customize it to your specifications, and provide a more refined user experience. Follow along as we break down the steps to install, configure, and enhance this LWC for your unique use case.
Improving Flow Screens with a Custom LWC Button Group
Let’s assume that you’re working on a flow screen that guides internal users to create donations within Salesforce. You have three types of donations: One-time, Recurring, and Pledges. On the first screen, you want your users to choose between these options and guide them through the appropriate screens based on the donation type.
If you use out-of-the-box Flow elements, it might look something like this:

![]()
Doesn’t that look much better? Let’s dive into how you can use this component in your flows and customize it to fit your needs.
1. Install the Unmanaged Package
To get started, install the unmanaged package from our GitHub repository. This package contains the LWC we’ll be using. You can find the installation link here.
Once installed, It’s ready to be used in your flow.
2. Create a Text Variable for the JSON String

Next, a JSON string needs to be passed to the LWC, which will generate the button group. Here’s an example of what that JSON might look like:
[
{
"value": "generalDonation",
"icon": "standard:thanks",
"label": "Enter One-time Donation",
"infoText": "A one-time donation made by an individual, organization, or business."
},
{
"value": "recurringDonation",
"icon": "standard:shift_scheduling_operation",
"label": "Setup Recurring Schedule",
"infoText": " An ongoing donation where the donor gives a fixed amount regularly, such as monthly or annually. Can be open-ended or with a fixed. "
},
{
"value": "pledge",
"icon": "standard:partners",
"label": "Record a Pledge (Commitment)",
"infoText": "A commitment to donate a specified amount to be paid in a lump sump or installments in the future."
}
]
Understanding the JSON:
- Value: This is the value that gets passed back to the Flow when the user selects a button.
- Icon: We’re using icons from the Salesforce Lightning Design System. You can browse the icon library here.
- Label: This label appears under the icon.
- InfoText: This text will appear when the user hovers over or selects a button, providing additional details.
You can adjust this JSON to add more buttons or change the icons, labels, or descriptions based on your specific requirements.
3. Add the LWC to Your Flow Screen

Now that you have the JSON ready, let’s add the Icon Button Group LWC to your Flow screen. When configuring the component, you’ll need to provide the following:
- Buttons JSON: The text variable is the one you created earlier.
- Default Selected Button: The value of the button you want to be selected by default. If left blank, no button will be selected when the screen loads.
- Header Text: Optional text that appears above the button group. Therefore, you can leave this blank if you’re using another component to display the header.
- Selected Button Value: This is where the selected value from the button group will be stored. Make sure to check the “Manually assign variables” option and create a text variable to capture the value.
And that’s it! If you’re curious to see how it all works under the hood, keep reading.
A Closer Look at the Code
This Lightning Web Component consists of four main files: the HTML, JavaScript, CSS, and XML files. Below, we’ll briefly explain each file. It is an unmanaged package, so proceed make modifications that fit your specific needs and branding.
HTML: iconButtonGroup.html
<template>
<div class="component-container">
<template if:true={headerText}>
<div class="header">
<p>Select Donation Type:</p>
</div>
</template>
<div class="button-group">
<template for:each={buttons} for:item="button">
<div key={button.value}
class={button.className}
onmouseover={handleMouseOver}
onclick={handleButtonClick}
data-value={button.value}>
<lightning-icon icon-name={button.icon} alternative-text={button.label} size="large" class="custom-icon"></lightning-icon>
<p class="label-text slds-text-title_caps slds-text-heading_small slds-m-top_medium">{button.label}</p>
</div>
</template>
</div>
<div class="info-area">
<p>{currentInfo}</p>
</div>
</div>
</template>
This file handles the structure of the component. This is where the component lays out the buttons based on the JSON you provide.
- We use lightning-button-icon components to create the buttons.
- A loop processes the JSON values to dynamically generate the button group.
You can modify the HTML if you want to change the layout or add extra elements, such as descriptions or tooltips.
JavaScript: IconButtonGroup.js
/**
* @file iconButtonGroup.js
* @description LWC component that displays a group of buttons, handles selection, and provides info based on the selected button. Outputs the selected value to the parent or a flow.
* @author Abdul-Rahman Haddam
* @company CirtaTech (cirtatech.com)
*/
import { LightningElement, api, track } from 'lwc';
export default class IconButtonGroup extends LightningElement {
@api buttonsJson; // Input: JSON string of buttons
@api defaultSelected; // Input: Default selected button value
@api headerText; // Input: Header
@track buttons = []; // Array of button objects parsed from JSON
@track selectedButton; // The value of the selected button
@track currentInfo; // The text to display in the info area
// Output property
@api selectedValue; // The currently selected value, to be passed back to the parent or flow
connectedCallback() {
console.log('Received buttonsJson:', this.buttonsJson);
try {
if (this.buttonsJson && this.buttonsJson.trim() !== '') {
this.buttons = JSON.parse(this.buttonsJson);
} else {
console.error('No valid buttonsJson received.');
}
} catch (error) {
console.error('Failed to parse buttons JSON:', error);
}
if (this.buttons.length > 0) {
this.buttons = this.buttons.map(button => {
return {
...button,
className: button.value === this.defaultSelected ? 'button selected' : 'button'
};
});
const defaultButton = this.buttons.find(button => button.value === this.defaultSelected);
if (defaultButton) {
this.selectedButton = defaultButton.value;
this.currentInfo = defaultButton.infoText;
this.selectedValue = defaultButton.value;
}
}
}
handleMouseOver(event) {
const hoveredValue = event.currentTarget.dataset.value;
const hoveredButton = this.buttons.find(button => button.value === hoveredValue);
if (hoveredButton) {
this.currentInfo = hoveredButton.infoText;
}
}
handleButtonClick(event) {
const clickedValue = event.currentTarget.dataset.value;
this.selectedButton = clickedValue;
this.buttons = this.buttons.map(button => {
return {
...button,
className: button.value === clickedValue ? 'button selected' : 'button'
};
});
const selectedButton = this.buttons.find(button => button.value === clickedValue);
if (selectedButton) {
this.currentInfo = selectedButton.infoText;
}
this.selectedValue = clickedValue;
const selectedEvent = new CustomEvent('buttonselect', {
detail: { value: this.selectedValue }
});
this.dispatchEvent(selectedEvent);
}
}
The JavaScript file controls the logic behind the component. It’s responsible for handling interactions like:
- Parsing the JSON to create buttons.
- Managing which button is selected.
- Passing the selected button’s value back to the Flow.
If you want to change how the button selections work or add additional behaviors (like disabling buttons based on certain conditions), then this is where you’ll make those changes.
CSS: iconButtonGroup.css
.component-container {
display: inline-block;
text-align: left;
margin: 0 auto;
width:100%;
}
.header {
text-align: left;
font-size: 1.5em;
font-weight: bold;
margin-bottom: 20px;
}
.button-group {
display: flex;
justify-content: center;
gap: 20px;
width: auto;
margin-bottom: 30px;
}
.button {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
width: 250px;
height: 150px;
padding: 15px;
cursor: pointer;
border-radius: 10px;
background-color: transparent;
color: #000000;
border: 2px solid transparent;
transition: all 0.3s ease-in-out;
}
.button:hover {
background-color: #eaf5fe;
}
.button.selected {
border: 2px solid #0070D2;
}
.custom-icon {
display: block;
border-radius: 50%;
--slds-c-icon-color-foreground-default: #ffffff;
--slds-c-icon-color-foreground: #ffffff;
--slds-c-icon-color-background-default: #084968;
--slds-c-icon-color-background: #084968;
padding: 10px;
}
.button p {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 12px;
text-align: center;
}
.info-area {
width: auto;
text-align: center;
font-size: 1.4em;
padding: 20px;
background-color: #eaf5fe;
border-radius: 8px;
box-sizing: border-box;
margin-top: 20px;
min-height: 96px;
}
Here, you can control the look and feel of the buttons. The CSS file includes styles to ensure the buttons are aligned properly, spaced evenly, and respond to hover and click events.
You can modify the CSS to match your organization’s branding or to adjust the sizing of the buttons.
XML: iconButtonGroup.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>58.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Icon Button Group</masterLabel>
<description>
A component that displays up to 5 buttons with icons, allowing selection of one button at a time.
The selected value is returned to the parent and descriptive information is shown on hover.
</description>
<targets>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__FlowScreen</target>
</targets>
<targetConfigs>
<targetConfig targets="lightning__FlowScreen">
<!-- Input properties -->
<property name="buttonsJson" type="String" label="Buttons JSON" description="A JSON string representing the list of buttons, including value, icon, label, and infoText." />
<property name="defaultSelected" type="String" label="Default Selected Button" description="The value of the default selected button." />
<property name="headerText" type="String" label="Header Text" description="Header text. If null no header will be displayed." />
<!-- Output property -->
<property name="selectedValue" type="String" role="outputOnly" label="Selected Button Value" description="Returns the value of the selected button." />
</targetConfig>
</targetConfigs>
</LightningComponentBundle>
This file defines the component’s metadata. It specifies where the component can be used (e.g., in Flows), what properties are exposed (like the button JSON), and more.
If you want to change the accessibility of the component or expose additional properties to the Flow, this is the file you’ll edit.
Conclusion
Feel free to “hack” into the code and tailor it to your needs. And if you run into any questions or want to see more examples, don’t hesitate to check out our GitHub repository or reach out in the comments!
Explore related content:
Dynamic Prefill for Salesforce Jotform Integration
