Collaborate, Innovate, Automate

MessageBanner — PnP Modern Search

The requirement was to deliver a banner that could be used for urgent announcements on the company intranet. If there are no announcements the webpart should not be visible.

A list-driven message banner for SharePoint intranets, built with PnP Modern Search and a custom Handlebars template. Supports multiple severity levels, multilingual content, and automatically minimises when there are no active messages.

English banner showing Medium severity in context on homepage

Overview

The MessageBanner displays urgent or important messages at the top of the intranet homepage. Messages are managed entirely through a SharePoint list — no page editing required. Content editors set a message to Active to show it, and Inactive to hide it. An optional expiration date can be set so messages stop showing automatically.

The solution uses two PnP Modern Search Results web parts — one on the English homepage and one on the French homepage — each pointing to the same list but configured with different slot mappings to display the correct language content.

The List — MessageBanner

MessageBanner list showing full column schema

The list contains the following columns:

Column Type Purpose Managed Property
Title Single line of text English message title Title
TitleFR Single line of text French message title RefinableString105
Description Multiple lines of text English message body Description
DescriptionFR Multiple lines of text French message body RefinableString106
SeeMore Hyperlink English link to related page
SeeMoreFR Hyperlink French link to related page
MessageLevel Choice (Low / Medium / High) Controls severity colour and badge RefinableString101
MessageStatus Choice (Active / Inactive) Controls visibility — filtered in query RefinableString100
ExpirationDate Date Optional — message stops showing after this date RefinableDate01

Severity Levels

Three severity levels drive the visual styling of the banner:

Web Part Configuration

First be sure to add the above columns including the RefinableStrings to the Selected Properties of the Search Results web part.

Query Template

Both the English and French web parts use the same query, scoped to the MessageBanner list and filtered to Active messages only:

{searchTerms} RefinableString100:"Active" Path:"https://yourtenant.sharepoint.com/sites/yoursite/Lists/MessageBanner"

RefinableString100 is the managed property mapped to the MessageStatus column.

Hide When Empty

The web part is configured with "Hide this web part if there's nothing to show" enabled. When no Active messages exist the web part takes up minimal space on the page — no empty containers or blank sections visible to users.

Layout Slots — English Web Part

Slot Name Mapped Property Source Column
Title Title English title
Description Description English description
Message Level RefinableString101 MessageLevel

Layout Slots — French Web Part

Slot Name Mapped Property Source Column
Title RefinableString105 TitleFR
DescriptionFR RefinableString106 DescriptionFR
Message Level RefinableString101 MessageLevel

Handlebars Templates

The templates are stored as .html files in a Site Assets library (PnP_CustomTemplates) and referenced via the External Template URL option in the web part. This means templates can be updated in one place without editing the web part on the page.

Site Assets folder showing BannerEng.html and BannerFR.html

The two templates are nearly identical. The key differences are:

Inserting the templates and Hiding the webpart

Inserting the Handlebars template and hiding the web part when empty

Handlebars Template — French

<style>
    .template--results {
        display: flex;
        flex-direction: column;
        gap: 1rem;
    }
    .warning-item {
        padding: 1rem;
        border-radius: 0.5rem;
        border-left-width: 4px;
        border-left-style: solid;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    }
    .warning-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        margin-bottom: 0.5rem;
    }
    .warning-header-left {
        display: flex;
        align-items: center;
    }
    .warning-level-badge {
        padding: 0.25rem 0.5rem;
        border-radius: 0.25rem;
        margin-right: 0.75rem;
        font-weight: bold;
        color: #ffffff;
        font-size: 0.875rem;
    }
    .warning-title {
        font-size: 1.125rem;
        font-weight: 600;
        color: #1a1a1a;
    }
    .warning-content {
        color: #4b5563;
    }
    .warning-more-btn {
        margin-left: auto;
        padding: 0.25rem 0.75rem;
        border-radius: 0.25rem;
        border: 1px solid #2563eb;
        color: #2563eb;
        font-size: 0.875rem;
        text-decoration: none;
        white-space: nowrap;
    }
    .warning-more-btn:hover {
        background-color: #2563eb;
        color: #ffffff;
    }
    /* Warning Level styles */
    [data-warning-level='High'] {
        border-left-color: #dc2626;
        background-color: #fef2f2;
    }
    [data-warning-level='High'] .warning-level-badge {
        background-color: #dc2626;
    }
    [data-warning-level='Medium'] {
        border-left-color: #f97316;
        background-color: #fff7ed;
    }
    [data-warning-level='Medium'] .warning-level-badge {
        background-color: #f97316;
    }
    [data-warning-level='Low'] {
        border-left-color: #16a34a;
        background-color: #f0fdf4;
    }
    [data-warning-level='Low'] .warning-level-badge {
        background-color: #16a34a;
    }
</style>
<div class="template">
    {{#if @root.properties.showSelectedFilters}}
        <pnp-selectedfilters
            data-filters="{{JSONstringify filters.selectedFilters 2}}"
            data-filters-configuration="{{JSONstringify filters.filtersConfiguration 2}}"
            data-instance-id="{{filters.instanceId}}"
            data-operator="{{filters.filterOperator}}"
            data-theme-variant="{{JSONstringify @root.theme}}"
        ></pnp-selectedfilters>
    {{/if}}
    <div class="template--header">
        {{#if @root.properties.showResultsCount}}
            <div class="template--resultCount">
                <label class="ms-fontWeight-semibold">{{getCountMessage @root.data.totalItemsCount @root.inputQueryText}}</label>
            </div>
        {{/if}}
        <div class="template--sort">
            <pnp-sortfield
                data-fields="{{JSONstringify @root.properties.dataSourceProperties.sortList}}"
                data-default-selected-field="{{sort.selectedSortFieldName}}"
                data-default-direction="{{sort.selectedSortDirection}}"
                data-theme-variant="{{JSONstringify @root.theme}}">
            </pnp-sortfield>
        </div>
    </div>
    <div class="template--results">
        {{#each data.items as |item|}}
            <pnp-select
                data-enabled="{{@root.properties.itemSelectionProps.allowItemSelection}}"
                data-index="{{@index}}"
                data-is-selected="{{isItemSelected @root.selectedKeys @index}}">
                <template id="content">
                    {{#> resultTypes item=item}}
                        <div class="warning-item" data-warning-level="{{slot item 'RefinableString101'}}">
                            <div class="warning-header">
                                <div class="warning-header-left">
                                    {{#if (eq (slot item 'RefinableString101') 'High')}}
                                        <span class="warning-level-badge">Élevé</span>
                                    {{else if (eq (slot item 'RefinableString101') 'Medium')}}
                                        <span class="warning-level-badge">Moyen</span>
                                    {{else}}
                                        <span class="warning-level-badge">Faible</span>
                                    {{/if}}
                                    <span class="warning-title">{{slot item @root.slots.Title}}</span>
                                </div>
                                <a href="{{slot item @root.slots.Path}}" class="warning-more-btn">En savoir plus →</a>
                            </div>
                            <div class="warning-content">
                                {{slot item @root.slots.DescriptionFR}}
                            </div>
                        </div>
                    {{/resultTypes}}
                </template>
            </pnp-select>
        {{/each}}
    </div>
</div>

See More button

A "See more" button sits on the right side of the banner header, linking to a related news page or announcement. The button only renders if the SeeMore slot is populated — messages without a linked page show no button.

French Homepage

French banner showing Élevé severity on French homepage in context

The French web part uses slot mappings to display TitleFR and DescriptionFR content, with the badge text hardcoded in French in the Handlebars template. The same MessageLevel value (High/Medium/Low) drives the colour styling on both web parts — only the displayed text differs.

Expiration Date

An optional ExpirationDate column allows messages to stop showing automatically without manual intervention. To implement automatic expiry, the query can be extended to filter on a mapped date managed property:

{searchTerms} RefinableString100:"Active" RefinableDate01>=today Path:"..."

Alternatively a scheduled Power Automate flow can run daily, check for items where ExpirationDate has passed, and set MessageStatus to Inactive — keeping the list data clean and giving editors clear visibility of what is and isn't active.

Want to create the list? Use the Create MessageBanner List PnP PowerShell script to provision the list and all required columns automatically.