In-app campaign with like/dislike swipe functionality

Published October 12, 2022
Modules
Difficulty

In-app campaigns are designed to enhance the customer experience in the mobile application. Thanks to them, on the one hand, we are able to present personalized products based on the analysis of customers behavior and activities. As a result, you always reach your target group with relevant, personalized messages tailored to each customer’s specific actions. We can also go a step further and give the customer the opportunity to decide for themselves what products they really like and what are their preferences.

One of such actions may be adding a product to your favorites. This activity can be used as a trigger for the campaign which displays the current bestsellers. Based on the swipe mechanism, customer can either like specific product or reject and see another product. In this way, we get to know the customer’s taste and preferences better. The products you show to your customers depend on your business needs. These could be bestsellers, but you can also specify a static group of products or use other type of AI recommendations.

Moreover, products marked as liked or disliked by the customer can be used in further communication with them. You can boost recommendation campaigns results with the liked products or filter the campaigns and exclude products that were disliked. The results of AI campaigns can be used in personalized mailings and in any other touchpoint, ensuring that you show customers only products they really like.

In this use case, we will describe the process of creating an in-app mechanism presenting bestsellers with the swiping system. The campaign will be displayed after the customer adds any product to the favorites. The customer has the option of swiping the products to the right or left and based on the customer decision an event with information that someone like/dislike the specific product is send to Synerise. The mechanism consists of two in-app campaigns: one to display the bar at the bottom of the screen encouraging to display bestsellers, and second one, appearing after clicking the bar, with the proper swiping system.

In-app message example

Prerequisites


  • Implement Synerise SDK (iOS or Android) in your mobile app.
  • Configure AI engine. Enable Top items recommendations.
  • Implement the product.addToFavorite event on Android and/or iOS. The event must be sent to Synerise after adding any product to favorites by the customer.
  • In Data Management > Events, configure the event actions that your in-app campaign will use to trigger displaying of the swipe mechanism and to send data about liking/disliking a product. In the JS SDK event settings section, you should set the way of authorizing these events to Make this event available to anonymous profiles without JWT. In this use case, the event actions are preferences.action and preferences.trigger.

Process


In this use case, you will go through the following steps:

  1. Create AI recommendations with bestsellers.
  2. Create an in-app campaign to propose the swiping mechanism presenting the bar encouraging to display bestsellers after adding any product to favorites.
  3. Create an in-app campaign to display the swiping mechanism presenting bestselling products with a swipe mechanism after clicking the bar.

Create AI recommendations


In this stage, you will create an AI recommendation campaign which will be used to display products in your in-app message. The customer will have the ability (implemented in the next stage by using an in-app campaign) to express liking or disliking the presented products.

  1. Go to Image presents the Communication icon Communication > Recommendations v2 > Add recommendation.

  2. Enter the name of the recommendation (it is only visible on the list of recommendations).

  3. In the Type & source section, click Define.

  4. From the Catalog dropdown list, select a product feed.

  5. Select the Top products recommendation type.

  6. Confirm the recommendation type by clicking Apply.

  7. In the Items section, click Define.

  8. Click Add slot.

  9. Click the Unnamed slot that was created.

  10. Define the minimum and maximum number of products displayed in the frame according to your needs.

  11. In Static filters, select the availability parameter and set it to is defined, so the recommendations will show only available items.

  12. Optionally, you can use filters to include specific items in the recommendation frame.

  13. Confirm the configuration by clicking Apply.

  14. In the Additional settings, click Define.

  15. From the Sort metric dropdown list, select Sold items count in the last 7 days.

  16. Confirm the configuration of Additional settings by clicking Apply.

  17. Click Save.

    AI recommendation configuration
    AI recommendation campaign configuration

Create an in-app campaign to propose the swiping mechanism


In this part of the process, you create an in-app campaign triggered by the product.addToFavorite event. After that, the bar encouraging to display bestsellers is displayed. The campaign lets the customer decide if they want to launch the swiping mechanism with the proposed products. The ready-to-use code presented below generates a custom event (preferences.trigger) after the customer clicks the bar. This event triggers the second in-app campaign, described further in this article.

  1. Go to Campaigns menu icon Communication > In-app messages > Create in-app

  2. Enter a meaningful name for the in-app campaign.

  3. In the Audience section:

    1. Click Define.
    2. Select the Everyone tab.
    3. Click Apply.
  4. In the Content section:

    1. Click Define.
    2. Click Create message and select Code Editor
    3. Create the content of your in-app campaign. You can reuse the code snippets presented below in your in-app template.
    Check the HTML code

      <div class="in-app-wrapper bottom_bar">
        <div class="in-app-close">
            <svg viewBox="0 0 24 24" class="close-m">
                <path fill="none" d="M0 0h24v24H0z"></path>
                <path
                    d="M13.06 12l4.72-4.72a.75.75 0 00-1.06-1.06L12 10.94 7.28 6.22a.75.75 0 00-1.06 1.06L10.94 12l-4.72 4.72a.75.75 0 101.06 1.06L12 13.06l4.72 4.72a.75.75 0 001.06-1.06z">
                </path>
            </svg>
        </div>
        <div class="in-app-content">
            <p class="in-app-title">Do you like our products?</p>
            <p class="in-app-subtitle">Let us know your taste ๐Ÿ”ฅ</p>
        </div>
    </div>
        

    Check the CSS code

        @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&display=swap");
        
        .in-app-wrapper.bottom_bar {
            margin: 15px;
            background-color: whitesmoke;
            height: 95px;
            border-radius: 30px;
            box-shadow: 0 0px 10px 0 rgba(35, 41, 54, 0.2);
    
            display: flex;
            flex-direction: column;
            flex-wrap: nowrap;
            align-content: center;
            justify-content: center;
            align-items: center;
        }
    
        .in-app-close {
            cursor: pointer;
            position: absolute;
            padding: 10px;
            top: 15px;
            right: 15px;
        }
    
        .in-app-close svg {
            width: 32px;
            height: 32px;
        }
    
        .in-app-content {
            text-align: center;
        }
    
        .in-app-title {
            margin: 0;
            font-weight: 600;
            font-size: 20px;
            margin-bottom: 2px;
            font-family: "Nunito", sans-serif;
        }
    
        .in-app-subtitle {
            margin: 0;
            font-size: 14px;
            font-family: "Nunito", sans-serif;
        }
        

    Check the JS code

        (function () {
            var inApp = document.querySelector('.in-app-wrapper.bottom_bar');
            var close = inApp.querySelector('.in-app-close');
            close.addEventListener('click', function () {
                SRInApp.close();
            })
    
            var content = inApp.querySelector('.in-app-content');
            content.addEventListener('click', function () {
                SRInApp.close();
                setTimeout(function() {
                    SRInApp.trackCustomEvent('preferences.trigger', {}, 'Preferences trigger event');
                }, 0) 
            })
        })();
        
    

  5. In the Trigger events section:

    1. Click Define.
    2. Select Add event and from the dropdown list, choose the product.addToFavorite event.
    3. Click the + where button and as the parameter, choose source.
    4. As the logical operator, select Contain and as the value add MOBILE to analyze events only from the mobile application.
    5. Click Apply.
    Trigger event settings
    Trigger event settings
  6. In the Schedule section:

    1. Click Define.
    2. Choose the Run immediately option.
    3. Click Apply.
  7. In the Display settings section:

    1. Click Change.
    2. Define the Delay display as 0 and Priority index as 2.
      Note: The mobile application can display one in-app message at a time. If the conditions allow the triggering of several in-apps at a time, the priority is a decisive factor for displaying the message. The messages with lower priority arenโ€™t queued for displaying after the first one is closed.
    3. Enable the Frequency limit toggle to manage the frequency of in-app message display in the application.
      In our case, we set it to once per day.
    4. Click Apply.
  8. Optionally, you can define the UTM parameters. Otherwise, click Skip step.

  9. Optionally, you can add the custom parameters in the Additional parameters section. Otherwise, click Skip step.

  10. To start your campaign, click Activate.

    In-app campaign settings
    In-app campaign settings

Create an in-app campaign to display the swiping mechanism


In this part of the process, you create an in-app campaign triggered by the preferences.trigger event. After that, the AI recommendation campaign with top products will be displayed. The campaign lets the customer swipe and like or reject displayed products. The ready-to-use code presented below generates a custom event telling you which products the customer liked/disliked.

  1. Go to Campaigns menu icon Communication > In-app messages > Create in-app

  2. Enter a meaningful name for the in-app campaign.

  3. In the Audience section:

    1. Click Define.
    2. Select the Everyone tab.
    3. Click Apply.
  4. In the Content section:

    1. Click Define.
    2. Click Create message and select Code Editor.
    3. In the right upper corner, change the Bottom bar default option to Fullscreen by using the dropdown menu.
    4. Create the content of your in-app campaign. You can reuse the code snippets presented below in your in-app template.
    Check the HTML code

        <div class="in-app-close" style="width: 100%;text-align: right;width: 90%;">
            <svg viewBox="0 0 24 24" class="close-m">
                <path fill="none" d="M0 0h24v24H0z"></path>
                <path
                    d="M13.06 12l4.72-4.72a.75.75 0 00-1.06-1.06L12 10.94 7.28 6.22a.75.75 0 00-1.06 1.06L10.94 12l-4.72 4.72a.75.75 0 101.06 1.06L12 13.06l4.72 4.72a.75.75 0 001.06-1.06z">
                </path>
            </svg>
        </div>
        <div class="frame"></div>
        <div class="icons">
            <svg id="hate" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.07 127.89">
                <path class="a"
                    d="M128.07,64.07c-.5,36.31-28,63.57-64,63.82S-.17,99.33,0,63.29C.17,28.25,29.23-.3,64.43,0A63.88,63.88,0,0,1,128.07,64.07ZM45.32,38.54c-2.44.36-4.63,1.12-6,3.68a6.39,6.39,0,0,0,.94,7.83A143,143,0,0,0,50.42,60.36c2.73,2.48,3.44,4.31.2,7a98.44,98.44,0,0,0-9.52,9.53c-3.62,4-3.66,7.48-.47,10.59,2.82,2.76,7.12,2.54,10.7-.79,3.05-2.83,5.91-5.86,8.85-8.8,2.58-2.57,5.16-2.53,7.73,0,2.83,2.81,5.62,5.67,8.52,8.42,3.87,3.68,8.08,4.08,11,1.15,3.23-3.21,3-6.85-.83-11C83.57,73.21,80.44,70,77.1,67c-2.37-2.13-2.71-3.65-.13-5.91,3.24-2.85,6.15-6.08,9.2-9.15,4.17-4.2,4.66-8,1.45-11.34-2.93-3-7.58-2.61-11.49,1.19-3.34,3.25-6.66,6.52-9.85,9.91-1.64,1.74-2.85,1.73-4.49,0-3.32-3.5-6.84-6.81-10.21-10.26A9,9,0,0,0,45.32,38.54Z" />
                <path
                    d="M45.32,38.54a9,9,0,0,1,6.26,2.87c3.37,3.45,6.89,6.76,10.21,10.26,1.64,1.73,2.85,1.74,4.49,0,3.19-3.39,6.51-6.66,9.85-9.91C80,38,84.69,37.52,87.62,40.57c3.21,3.34,2.72,7.14-1.45,11.34-3,3.07-6,6.3-9.2,9.15-2.58,2.26-2.24,3.78.13,5.91,3.34,3,6.47,6.24,9.53,9.52,3.87,4.16,4.06,7.8.83,11-2.95,2.93-7.16,2.53-11-1.15-2.9-2.75-5.69-5.61-8.52-8.42-2.57-2.54-5.15-2.58-7.73,0-2.94,2.94-5.8,6-8.85,8.8-3.58,3.33-7.88,3.55-10.7.79-3.19-3.11-3.15-6.6.47-10.59a98.44,98.44,0,0,1,9.52-9.53c3.24-2.72,2.53-4.55-.2-7A143,143,0,0,1,40.28,50.05a6.39,6.39,0,0,1-.94-7.83C40.69,39.66,42.88,38.9,45.32,38.54Z" />
            </svg>
            <svg id="like" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128.06 127.99">
                <path class="a"
                    d="M128.06,63.83a63.65,63.65,0,0,1-64,64.16A63.57,63.57,0,0,1,0,64a64,64,0,0,1,128.06-.13ZM96,56.53c0-5.82-3.9-13.3-10.19-16.05-6.9-3-13.67-2.67-19.37,2.82-2,1.9-3.16,1.41-4.93-.17-2.34-2.08-4.86-3.89-8.25-4.24-9.13-.92-15.31,2.3-19.11,10.25-3.89,8.11-2.42,17.27,4,23.34,7.5,7,15.22,13.88,22.77,20.89,2.06,1.92,3.76,2.27,6,.21C74.36,86.7,82,80,89.39,73.09,93.57,69.21,96.06,64.45,96,56.53Z" />
                <path
                    d="M96,56.53c.08,7.92-2.41,12.68-6.59,16.56C82,80,74.36,86.7,66.93,93.58c-2.23,2.06-3.93,1.71-6-.21-7.55-7-15.27-13.84-22.77-20.89-6.46-6.07-7.93-15.23-4-23.34,3.8-8,10-11.17,19.11-10.25,3.39.35,5.91,2.16,8.25,4.24,1.77,1.58,2.95,2.07,4.93.17,5.7-5.49,12.47-5.84,19.37-2.82C92.08,43.23,96,50.71,96,56.53Z" />
            </svg>
        </div>
        

    Check the CSS code

           @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;900&display=swap");
    
            html,
            body {
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                width: 100%;
                height: 100%;
                overflow: hidden;
                font-family: "Nunito", sans-serif;
                background-color: rgba(0, 0, 0, 0.4);
            }
    
            .frame {
                position: relative;
                width: 90%;
                height: 70%;
                max-width: 400px;
                max-height: 600px;
                z-index: 1;
            }
    
            .icons {
                margin-top: 3vh;
                user-select: none;
                z-index: 1;
            }
    
            .icons>svg {
                width: 10vh;
                height: 10vh;
                max-width: 60px;
                max-height: 60px;
                border-radius: 50%;
                box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
                cursor: pointer;
            }
    
            .icons>svg:nth-child(1) {
                fill: #fb4f68;
                margin-right: 2vh;
            }
    
            .icons>svg:nth-child(2) {
                fill: #4dca93;
            }
    
            .icons>svg>path:nth-child(1) {
                fill: #fff;
            }
    
            .card {
                background-color: #fff;
                position: absolute;
                display: flex;
                align-items: flex-end;
                justify-content: center;
                width: 100%;
                height: 100%;
                color: #f1f1f1;
                border-radius: 10px;
                user-select: none;
                cursor: pointer;
                overflow: hidden;
                background-size: cover;
                background-repeat: no-repeat;
                background-position: center;
                touch-action: none;
            }
    
            .card .is-like {
                width: 100%;
                height: 100%;
                position: absolute;
                opacity: 0;
            }
    
            .card .is-like::after {
                position: absolute;
                left: 50%;
                bottom: 30%;
                transform: translateX(-50%) rotate(-10deg);
                width: 30%;
                height: 13%;
                font-size: 3em;
                letter-spacing: 0.2em;
                font-weight: 600;
                border-radius: 0.15em;
                display: flex;
                align-items: center;
                justify-content: center;
            }
    
            .card .like::after {
                content: "๐Ÿ‘";
            }
    
            .card .nope::after {
                content: "๐Ÿ‘Ž";
            }
    
            .card .bottom {
                width: 100%;
                height: 25%;
                background: linear-gradient(to top, #000000b8, #00000000);
                display: flex;
                flex-direction: column;
                align-items: flex-start;
                justify-content: center;
                padding-left: 7%;
                font-weight: 400;
                padding-bottom: 25px
            }
    
            .card .bottom .title>span:nth-child(1) {
                font-size: 2em;
                font-weight: 700;
                margin-right: 0.2em;
            }
    
            .card .bottom .title>span:nth-child(2) {
                font-size: clamp(13px, 1.5em, 25px);
            }
    
            .card .bottom .title>span:nth-child(2)>b {
                font-size: 0.6em;
                margin-right: 0.2em;
            }
    
            .card .bottom .info {
                margin: 0;
            }
    
            @media screen and (max-height: 540px) {
                .frame {
                    width: 90%;
                    height: 70%;
                    font-size: 13px;
                }
            }
    
            @media screen and (max-height: 440px) {
                .frame {
                    font-size: 8px;
                }
            }
    
            .in-app-close svg {
                width: 32px;
                height: 32px;
                cursor: pointer;
                background-color: #000;
                fill: #fff;
            }
    
            .in-app-close {
                max-width: 400px;
                margin-bottom: 16px;
            }
        

    Check the JS code

            let imgCount = 0;
            const data = {% set arr = [] %}{% recommendations3 campaignId=CAMPAIGN-ID %}{% for p in recommended_products3 %}{% set obj = {itemId: p.itemId, img: p.image_link, name: p.title, price: p.price, distance: p.category} %}{% do arr.append(obj) %}{% endfor %}{{arr|tojson}}{% endrecommendations3 %}
    
    
    
            const frame = document.body.querySelector(".frame");
            data.forEach((_data) => appendCard(_data));
    
            let current = frame.querySelector(".card:last-child");
            let likeText = current.children[0];
            let startX = 0,
                startY = 0,
                moveX = 0,
                moveY = 0;
    
            document.querySelector("#like").onclick = () => {
                moveX = 1;
                moveY = 0;
                complete('like');
            };
    
            document.querySelector("#hate").onclick = () => {
                moveX = -1;
                moveY = 0;
                complete('hate');
            };
    
            function appendCard(data) {
                const firstCard = frame.children[0];
                const newCard = document.createElement("div");
                newCard.className = "card";
                newCard.setAttribute('data-sku', data.itemId);
                newCard.setAttribute('data-brand', data.name);
                newCard.setAttribute('data-title', data.distance);
                newCard.style.backgroundImage = `url(${data.img})`;
                newCard.innerHTML = `
                    <div class="is-like">LIKE</div>
                    <div class="bottom">
                      <div class="title">
                        <span>${data.name}</span>
                        <span>${data.price}<b>zล‚</b></span>
                      </div>
                      <div class="info">
                        ${data.distance}
                      </div>
                    </div>
                  `;
                if (firstCard) frame.insertBefore(newCard, firstCard);
                else frame.appendChild(newCard);
                imgCount++;
            }
    
            function initCard(card) {
                card.addEventListener("pointerdown", onPointerDown);
            }
    
            function setTransform(x, y, deg, duration) {
                current.style.transform = `translate3d(${x}px, ${y}px, 0) rotate(${deg}deg)`;
                likeText.style.opacity = Math.abs((x / innerWidth) * 2.1);
                likeText.className = `is-like ${x > 0 ? "like" : "nope"}`;
                if (duration) current.style.transition = `transform ${duration}ms`;
            }
    
            function onPointerDown({
                clientX,
                clientY
            }) {
                startX = clientX;
                startY = clientY;
                current.addEventListener("pointermove", onPointerMove);
                current.addEventListener("pointerup", onPointerUp);
                current.addEventListener("pointerleave", onPointerUp);
            }
    
            function onPointerMove({
                clientX,
                clientY
            }) {
                moveX = clientX - startX;
                moveY = clientY - startY;
                setTransform(moveX, moveY, (moveX / innerWidth) * 50);
            }
    
            function onPointerUp() {
                current.removeEventListener("pointermove", onPointerMove);
                current.removeEventListener("pointerup", onPointerUp);
                current.removeEventListener("pointerleave", onPointerUp);
                if (Math.abs(moveX) > frame.clientWidth / 2) {
                    current.removeEventListener("pointerdown", onPointerDown);
                    complete(String(moveX).match('-') ? 'hate' : 'like');
                } else cancel();
            }
    
            function complete(type) {
                try {
                    const flyX = (Math.abs(moveX) / moveX) * innerWidth * 1.3;
                    const flyY = (moveY / moveX) * flyX;
                    setTransform(flyX, flyY, (flyX / innerWidth) * 50, innerWidth);
    
                    SRInApp.trackCustomEvent('preferences.action', {
                        actionType: type,
                        itemId: current.getAttribute('data-sku'),
                        title: current.getAttribute('data-title'),
                        brand: current.getAttribute('data-brand')
                    }, 'Preferences action');
    
                    const prev = current;
                    const next = current.previousElementSibling;
                    if (next) initCard(next);
                    current = next;
                    likeText = current.children[0];
                    // appendCard(data[imgCount % 4]);
                    setTimeout(() => frame.removeChild(prev), innerWidth);
                } catch(error) {
                    document.querySelector('body').style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
                    document.querySelector('body').innerHTML = '<h1 style="color: #fff; width: 90%; max-width: 400px;">Thank you!<br>We will reach out to you</h1>';
                    setTimeout(function(){
                        SRInApp.close();
                    }, 3000)
                }
            }
    
            function cancel() {
                setTransform(0, 0, 0, 100);
                setTimeout(() => (current.style.transition = ""), 100);
            }
    
            var close = document.querySelector('.in-app-close svg');
            close.addEventListener('click', function () {
                SRInApp.close();
            })
    
            initCard(current);
        
    

    Important: In the JS code section, replace the CAMPAIGN-ID with the ID of your campaign created in the previous step.
  5. In the Trigger events section:

    1. Click Define.

    2. Select Add event and from the dropdown list, choose the preferences.trigger event.

    3. Click the + where button and as the parameter, choose source.

      Note: If you cannot find the source parameter on the dropdown list, go to Data Management > Events, find preferences.trigger event on the list and add cource parameter by clicking Add property button.

    4. As the logical operator, select Contain and as the value add MOBILE to analyze events only from the mobile application.

    5. Click Apply.

  6. In the Schedule section:

    1. Click Define.
    2. Choose Run immediately option.
    3. Click Apply.
  7. In the Display settings section:

    1. Click Change.
    2. Define the Delay display as 0 and Priority index as 1.
      Note: The mobile application can display one in-app message at a time. If the conditions allow the display of several in-apps at a time, the priority is a decisive factor for displaying the message. The messages with lower priority arenโ€™t queued.
    3. Click Apply.
  8. Optionally, you can define the UTM parameters. Otherwise, click Skip step.

  9. Optionally, you can add the custom parameters in the Additional parameters section. Otherwise, click Skip step.

  10. To start your campaign, click Activate.

What’s next


After the campaign is implemented, the event preferences.action is added on the client’s card. This event has an itemId (product id) and actionType parameter with a value like or hate.

Based on this event, you can create an aggregate with products liked or rejected by the customer. Remember, that before creating an aggregate, you should either have this event already somewhere in your history, or have its definition added in Data Management module in Synerise. Otherwise it will not be available in analysts.

Check how to build this aggregate

  1. Go to Analytics iconAnalytics > Aggregates > New aggregate.
  2. Enter the name of the aggregate.
  3. As the type of the aggregate select Last Multi.
  4. In the Size field choose how many values you would like to return - in this case it might be 100.
  5. Select Consider only distinct occurrence of the event parameter.
  6. Select the preferences.action event.
  7. Select the itemId parameter.
  8. Click the + where button, and add the actionType parameter.
  9. Choose operator Equal.
  10. Define the parameter value as like.
  11. Save the aggregate.
Configuration of the aggregate
Configuration of the aggregate
Note: To create an aggregate with product rejected by the customer, duplicate the aggregate created above and change the value of the parameter to hate.

Here are some ideas for using the aggregate:

  • You can add such an aggregate to the Boosting section in other recommendation campaigns and enrich the proposed recommendations with products that have been liked by the customer, or to exclude those which they have rejected.
  • You can use the aggregate with the customer’s favorite products and display them as the section on the website or in the cart, as an incentive to buy products that the customer liked and in this way - increase the value of the basket.
  • You can use this aggregate also in the price drop campaign. If the customer does not buy a specific product they liked and its price drops, you can send them this information to encourage them to come back and make a purchase.
  • You can use disliked products in AI search filters, and in this way - exclude those products from the search results.

Check the use case set up on the Synerise Demo workspace


You can check the configuration of the campaign components directly in Synerise Demo workspace:

If you don’t have access to the Synerise Demo workspace, please leave your contact details in this form, and our representative will contact you shortly.

Read more


๐Ÿ˜•

We are sorry to hear that

Thank you for helping improve out documentation. If you need help or have any questions, please consider contacting support.

๐Ÿ˜‰

Awesome!

Thank you for helping improve out documentation. If you need help or have any questions, please consider contacting support.

Close modal icon Placeholder alt for modal to satisfy link checker