Applying sort and filters
This pattern explains how to implement server-side filtering and sorting in Power Pages using URL parameters. When users apply filters, the page reloads with the filter values in the URL, which are then used to query Dataverse and display filtered results.
Overview
The filter and sort pattern follows this flow:
- User selects filter options from the filter panel component
- User clicks "Apply" which reloads the page with filter values as URL parameters
- Server-side Liquid code reads the URL parameters
- FetchXML query is built dynamically based on the parameters
- Filtered results are displayed along with a "Filters applied" summary
- Users can remove individual filters or clear all filters
This approach is preferred over client-side filtering because:
- it works with large datasets without loading all records
- filter state is preserved in the URL (shareable, bookmarkable)
- it works without JavaScript for basic functionality
- it's more performant for complex queries
How URL parameters work
When the user applies filters, the form submits and appends the selected values to the URL as query parameters. For example:
/search?order=updated-newest&status[]=active&status[]=pending&topic=education
In this URL:
order=updated-newest- a single value parameter (sort order)status[]=active&status[]=pending- multiple values for the same parameter (checkboxes)topic=education- a single value parameter (select dropdown)
The filter panel must be wrapped in a form using the GET method. This ensures filter values are appended to the URL as query parameters when the user clicks Apply.
Reading URL parameters in Liquid
In Power Pages, URL parameters are accessed via the request.params object. You can read single values or handle arrays for multi-select filters.
Reading single value parameters
For parameters like sort order or select dropdowns that have a single value:
{% comment %}
Reading single value parameters from the URL
{% endcomment %}
{% assign sort_order = request.params['order'] | default: 'relevance' %}
{% assign selected_topic = request.params['topic'] %}
{% assign keywords = request.params['keywords'] %}
{% comment %}
Use the values in your template
{% endcomment %}
{% if selected_topic %}
<p>Showing results for topic: {{ selected_topic }}</p>
{% endif %}
Reading multi-value parameters (checkboxes)
For checkbox groups where multiple values can be selected, the values come through as a comma-separated string. Use Liquid's split filter to convert to an array:
{% comment %}
Reading multi-value parameters (from checkboxes)
Values come as comma-separated string: "active,pending,closed"
{% endcomment %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% comment %}
Check if a specific value is selected
{% endcomment %}
{% assign has_active = false %}
{% for status in selected_statuses %}
{% if status == 'active' %}
{% assign has_active = true %}
{% endif %}
{% endfor %}
{% comment %}
Count how many filters are active
{% endcomment %}
{% assign filter_count = selected_statuses | size %}
Setting form values from URL parameters
When the page loads with URL parameters, you need to pre-select the corresponding form inputs so users can see what filters are active:
{% comment %}
Pre-select checkboxes based on URL parameters
{% endcomment %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
<div class="govuk-checkboxes govuk-checkboxes--small">
<div class="govuk-checkboxes__item">
<input type="checkbox"
name="status[]"
id="status-active"
value="active"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'active' %}checked{% endif %}>
<label for="status-active" class="govuk-label govuk-checkboxes__label">Active</label>
</div>
<div class="govuk-checkboxes__item">
<input type="checkbox"
name="status[]"
id="status-pending"
value="pending"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'pending' %}checked{% endif %}>
<label for="status-pending" class="govuk-label govuk-checkboxes__label">Pending review</label>
</div>
<div class="govuk-checkboxes__item">
<input type="checkbox"
name="status[]"
id="status-closed"
value="closed"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'closed' %}checked{% endif %}>
<label for="status-closed" class="govuk-label govuk-checkboxes__label">Closed</label>
</div>
</div>
{% comment %}
Pre-select radio buttons based on URL parameters
{% endcomment %}
{% assign sort_order = request.params['order'] | default: 'relevance' %}
<div class="govuk-radios govuk-radios--small">
<div class="govuk-radios__item">
<input type="radio"
name="order"
id="sort-relevance"
value="relevance"
class="govuk-radios__input"
{% if sort_order == 'relevance' %}checked{% endif %}>
<label for="sort-relevance" class="govuk-label govuk-radios__label">Relevance</label>
</div>
<div class="govuk-radios__item">
<input type="radio"
name="order"
id="sort-newest"
value="updated-newest"
class="govuk-radios__input"
{% if sort_order == 'updated-newest' %}checked{% endif %}>
<label for="sort-newest" class="govuk-label govuk-radios__label">Updated (newest)</label>
</div>
<div class="govuk-radios__item">
<input type="radio"
name="order"
id="sort-oldest"
value="updated-oldest"
class="govuk-radios__input"
{% if sort_order == 'updated-oldest' %}checked{% endif %}>
<label for="sort-oldest" class="govuk-label govuk-radios__label">Updated (oldest)</label>
</div>
</div>
{% comment %}
Pre-select dropdown based on URL parameters
{% endcomment %}
{% assign selected_topic = request.params['topic'] %}
<div class="govuk-form-group">
<label for="topic" class="govuk-label">Topic</label>
<select name="topic" id="topic" class="govuk-select govuk-!-width-full">
<option value="" {% if selected_topic == blank %}selected{% endif %}>All topics</option>
<option value="business" {% if selected_topic == 'business' %}selected{% endif %}>Business and industry</option>
<option value="education" {% if selected_topic == 'education' %}selected{% endif %}>Education, training and skills</option>
<option value="health" {% if selected_topic == 'health' %}selected{% endif %}>Health and social care</option>
<option value="money" {% if selected_topic == 'money' %}selected{% endif %}>Money</option>
<option value="transport" {% if selected_topic == 'transport' %}selected{% endif %}>Transport</option>
</select>
</div>
Building FetchXML queries
Use the URL parameters to dynamically build your FetchXML query. This allows you to filter and sort the data based on user selections.
Basic query with sorting
{% comment %}
Build FetchXML with dynamic sorting based on URL parameters
{% endcomment %}
{% assign sort_order = request.params['order'] | default: 'relevance' %}
{% comment %}
Map sort parameter to FetchXML order attributes
{% endcomment %}
{% case sort_order %}
{% when 'updated-newest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'true' %}
{% when 'updated-oldest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'false' %}
{% when 'name-asc' %}
{% assign sort_attribute = 'dfe_name' %}
{% assign sort_descending = 'false' %}
{% when 'name-desc' %}
{% assign sort_attribute = 'dfe_name' %}
{% assign sort_descending = 'true' %}
{% else %}
{% comment %} Default to relevance/created date {% endcomment %}
{% assign sort_attribute = 'createdon' %}
{% assign sort_descending = 'true' %}
{% endcase %}
{% fetchxml records %}
<fetch>
<entity name="dfe_record">
<attribute name="dfe_recordid" />
<attribute name="dfe_name" />
<attribute name="modifiedon" />
<order attribute="{{ sort_attribute }}" descending="{{ sort_descending }}" />
</entity>
</fetch>
{% endfetchxml %}
Query with filter conditions
Add filter conditions based on the URL parameters. Use the condition element for single values and loop through arrays for multi-select filters:
{% comment %}
Build FetchXML filter conditions from URL parameters
{% endcomment %}
{% assign selected_topic = request.params['topic'] %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% fetchxml records %}
<fetch>
<entity name="dfe_record">
<attribute name="dfe_recordid" />
<attribute name="dfe_name" />
<attribute name="dfe_topic" />
<attribute name="statuscode" />
<filter type="and">
{% comment %}
Single value filter - only add if parameter has a value
{% endcomment %}
{% if selected_topic != blank %}
<condition attribute="dfe_topic" operator="eq" value="{{ selected_topic }}" />
{% endif %}
{% comment %}
Multi-value filter - use OR condition for checkbox groups
{% endcomment %}
{% if selected_statuses.size > 0 %}
<filter type="or">
{% for status in selected_statuses %}
<condition attribute="statuscode" operator="eq" value="{{ status }}" />
{% endfor %}
</filter>
{% endif %}
</filter>
</entity>
</fetch>
{% endfetchxml %}
Complete FetchXML example
Here's a complete example combining sorting and multiple filter types:
{% comment %}
Complete FetchXML query with sorting and multiple filter types
{% endcomment %}
{% comment %} Read URL parameters {% endcomment %}
{% assign sort_order = request.params['order'] | default: 'updated-newest' %}
{% assign selected_topic = request.params['topic'] %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% assign type_param = request.params['type[]'] %}
{% assign selected_types = type_param | split: ',' %}
{% assign keywords = request.params['keywords'] %}
{% comment %} Determine sort order {% endcomment %}
{% case sort_order %}
{% when 'updated-newest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'true' %}
{% when 'updated-oldest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'false' %}
{% else %}
{% assign sort_attribute = 'createdon' %}
{% assign sort_descending = 'true' %}
{% endcase %}
{% comment %} Check if any filters are active {% endcomment %}
{% assign has_filters = false %}
{% if selected_topic != blank %}{% assign has_filters = true %}{% endif %}
{% if selected_statuses.size > 0 %}{% assign has_filters = true %}{% endif %}
{% if selected_types.size > 0 %}{% assign has_filters = true %}{% endif %}
{% if keywords != blank %}{% assign has_filters = true %}{% endif %}
{% fetchxml records %}
<fetch>
<entity name="dfe_record">
<attribute name="dfe_recordid" />
<attribute name="dfe_name" />
<attribute name="dfe_description" />
<attribute name="dfe_topic" />
<attribute name="dfe_type" />
<attribute name="statuscode" />
<attribute name="modifiedon" />
<attribute name="createdon" />
<order attribute="{{ sort_attribute }}" descending="{{ sort_descending }}" />
{% if has_filters %}
<filter type="and">
{% comment %} Keyword search {% endcomment %}
{% if keywords != blank %}
<filter type="or">
<condition attribute="dfe_name" operator="like" value="%{{ keywords }}%" />
<condition attribute="dfe_description" operator="like" value="%{{ keywords }}%" />
</filter>
{% endif %}
{% comment %} Topic filter (single select) {% endcomment %}
{% if selected_topic != blank %}
<condition attribute="dfe_topic" operator="eq" value="{{ selected_topic }}" />
{% endif %}
{% comment %} Status filter (multi-select checkboxes) {% endcomment %}
{% if selected_statuses.size > 0 %}
<filter type="or">
{% for status in selected_statuses %}
<condition attribute="statuscode" operator="eq" value="{{ status }}" />
{% endfor %}
</filter>
{% endif %}
{% comment %} Type filter (multi-select checkboxes) {% endcomment %}
{% if selected_types.size > 0 %}
<filter type="or">
{% for type in selected_types %}
<condition attribute="dfe_type" operator="eq" value="{{ type }}" />
{% endfor %}
</filter>
{% endif %}
</filter>
{% endif %}
</entity>
</fetch>
{% endfetchxml %}
Date filters
Date filters require special handling because the GOV.UK date input component uses three separate fields (day, month, year) which come through as separate URL parameters with bracket notation.
How date parameters appear in the URL
When a user enters a date, the URL will contain parameters like:
/search?date_from[day]=28&date_from[month]=2&date_from[year]=2024&date_to[day]=13&date_to[month]=12&date_to[year]=2025
Reading date parameters
Read each date component separately and combine them into an ISO date format (YYYY-MM-DD) for use in FetchXML queries:
{% comment %}
Reading date parameters from URL
Date inputs use bracket notation: date_from[day], date_from[month], date_from[year]
Each component comes through as a separate parameter
{% endcomment %}
{% comment %} Read the individual date components {% endcomment %}
{% assign date_from_day = request.params['date_from[day]'] %}
{% assign date_from_month = request.params['date_from[month]'] %}
{% assign date_from_year = request.params['date_from[year]'] %}
{% assign date_to_day = request.params['date_to[day]'] %}
{% assign date_to_month = request.params['date_to[month]'] %}
{% assign date_to_year = request.params['date_to[year]'] %}
{% comment %} Check if a complete date was provided {% endcomment %}
{% assign has_date_from = false %}
{% if date_from_day != blank and date_from_month != blank and date_from_year != blank %}
{% assign has_date_from = true %}
{% endif %}
{% assign has_date_to = false %}
{% if date_to_day != blank and date_to_month != blank and date_to_year != blank %}
{% assign has_date_to = true %}
{% endif %}
{% comment %}
Build ISO date strings for FetchXML (YYYY-MM-DD format)
Pad day and month with leading zeros if needed
{% endcomment %}
{% if has_date_from %}
{% assign padded_day = date_from_day | prepend: '0' | slice: -2, 2 %}
{% assign padded_month = date_from_month | prepend: '0' | slice: -2, 2 %}
{% assign date_from_iso = date_from_year | append: '-' | append: padded_month | append: '-' | append: padded_day %}
{% endif %}
{% if has_date_to %}
{% assign padded_day = date_to_day | prepend: '0' | slice: -2, 2 %}
{% assign padded_month = date_to_month | prepend: '0' | slice: -2, 2 %}
{% assign date_to_iso = date_to_year | append: '-' | append: padded_month | append: '-' | append: padded_day %}
{% endif %}
Pre-populating date inputs
When the page loads with date parameters in the URL, populate each input field with its corresponding value:
{% comment %}
Pre-populate date inputs from URL parameters
{% endcomment %}
{% assign date_from_day = request.params['date_from[day]'] %}
{% assign date_from_month = request.params['date_from[month]'] %}
{% assign date_from_year = request.params['date_from[year]'] %}
<div class="govuk-form-group">
<fieldset class="govuk-fieldset" role="group" aria-describedby="date-from-hint">
<legend class="govuk-fieldset__legend">Updated after</legend>
<div id="date-from-hint" class="govuk-hint govuk-!-margin-bottom-2">
For example, 28 2 2024
</div>
<div class="govuk-date-input" id="date-from">
<div class="govuk-date-input__item">
<div class="govuk-form-group">
<label for="date-from-day" class="govuk-label">Day</label>
<input class="govuk-input govuk-input--width-2"
id="date-from-day"
name="date_from[day]"
type="text"
inputmode="numeric"
value="{{ date_from_day }}">
</div>
</div>
<div class="govuk-date-input__item">
<div class="govuk-form-group">
<label for="date-from-month" class="govuk-label">Month</label>
<input class="govuk-input govuk-input--width-2"
id="date-from-month"
name="date_from[month]"
type="text"
inputmode="numeric"
value="{{ date_from_month }}">
</div>
</div>
<div class="govuk-date-input__item">
<div class="govuk-form-group">
<label for="date-from-year" class="govuk-label">Year</label>
<input class="govuk-input govuk-input--width-4"
id="date-from-year"
name="date_from[year]"
type="text"
inputmode="numeric"
value="{{ date_from_year }}">
</div>
</div>
</div>
</fieldset>
</div>
FetchXML date conditions
Use the on-or-after operator for "from" dates and on-or-before for "to" dates. The date value must be in ISO format (YYYY-MM-DD):
{% comment %}
FetchXML with date filter conditions
Use 'on-or-after' for "from" dates and 'on-or-before' for "to" dates
Date values must be in ISO format (YYYY-MM-DD)
{% endcomment %}
{% comment %} Read and build date values (see readDateParam example) {% endcomment %}
{% assign date_from_day = request.params['date_from[day]'] %}
{% assign date_from_month = request.params['date_from[month]'] %}
{% assign date_from_year = request.params['date_from[year]'] %}
{% assign date_to_day = request.params['date_to[day]'] %}
{% assign date_to_month = request.params['date_to[month]'] %}
{% assign date_to_year = request.params['date_to[year]'] %}
{% assign has_date_from = false %}
{% if date_from_day != blank and date_from_month != blank and date_from_year != blank %}
{% assign has_date_from = true %}
{% assign padded_day = date_from_day | prepend: '0' | slice: -2, 2 %}
{% assign padded_month = date_from_month | prepend: '0' | slice: -2, 2 %}
{% assign date_from_iso = date_from_year | append: '-' | append: padded_month | append: '-' | append: padded_day %}
{% endif %}
{% assign has_date_to = false %}
{% if date_to_day != blank and date_to_month != blank and date_to_year != blank %}
{% assign has_date_to = true %}
{% assign padded_day = date_to_day | prepend: '0' | slice: -2, 2 %}
{% assign padded_month = date_to_month | prepend: '0' | slice: -2, 2 %}
{% assign date_to_iso = date_to_year | append: '-' | append: padded_month | append: '-' | append: padded_day %}
{% endif %}
{% fetchxml records %}
<fetch>
<entity name="dfe_record">
<attribute name="dfe_recordid" />
<attribute name="dfe_name" />
<attribute name="modifiedon" />
<order attribute="modifiedon" descending="true" />
{% if has_date_from or has_date_to %}
<filter type="and">
{% if has_date_from %}
<condition attribute="modifiedon" operator="on-or-after" value="{{ date_from_iso }}" />
{% endif %}
{% if has_date_to %}
<condition attribute="modifiedon" operator="on-or-before" value="{{ date_to_iso }}" />
{% endif %}
</filter>
{% endif %}
</entity>
</fetch>
{% endfetchxml %}
Common FetchXML date operators:
on-or-after- date is on or after the specified valueon-or-before- date is on or before the specified valueon- date matches exactlytoday- date is today (no value needed)last-x-days- date is within the last X days
Showing date filters in the summary
Display applied date filters in a user-friendly format. When building the remove URL, exclude all three date components (day, month, year):
{% comment %}
Render date filters in the filter summary
{% endcomment %}
{% assign date_from_day = request.params['date_from[day]'] %}
{% assign date_from_month = request.params['date_from[month]'] %}
{% assign date_from_year = request.params['date_from[year]'] %}
{% assign date_to_day = request.params['date_to[day]'] %}
{% assign date_to_month = request.params['date_to[month]'] %}
{% assign date_to_year = request.params['date_to[year]'] %}
{% assign has_date_from = false %}
{% if date_from_day != blank and date_from_month != blank and date_from_year != blank %}
{% assign has_date_from = true %}
{% assign date_from_display = date_from_day | append: '/' | append: date_from_month | append: '/' | append: date_from_year %}
{% endif %}
{% assign has_date_to = false %}
{% if date_to_day != blank and date_to_month != blank and date_to_year != blank %}
{% assign has_date_to = true %}
{% assign date_to_display = date_to_day | append: '/' | append: date_to_month | append: '/' | append: date_to_year %}
{% endif %}
{% comment %}
Build URLs that remove date filters
We need to exclude all three date components (day, month, year)
{% endcomment %}
{% if has_date_from or has_date_to %}
<ul class="dfe-filter-summary__list">
{% if has_date_from %}
{% capture remove_date_from_url %}{{ page.url }}?{% for param in request.params %}{% unless param[0] contains 'date_from' %}{{ param[0] }}={{ param[1] | url_encode }}&{% endunless %}{% endfor %}{% endcapture %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-label">Updated after:</span>
<span class="dfe-filter-summary__tag-value">{{ date_from_display }}</span>
<a href="{{ remove_date_from_url | strip }}"
class="dfe-filter-summary__tag-remove">
<span class="govuk-visually-hidden">Remove updated after filter</span>
×
</a>
</li>
{% endif %}
{% if has_date_to %}
{% capture remove_date_to_url %}{{ page.url }}?{% for param in request.params %}{% unless param[0] contains 'date_to' %}{{ param[0] }}={{ param[1] | url_encode }}&{% endunless %}{% endfor %}{% endcapture %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-label">Updated before:</span>
<span class="dfe-filter-summary__tag-value">{{ date_to_display }}</span>
<a href="{{ remove_date_to_url | strip }}"
class="dfe-filter-summary__tag-remove">
<span class="govuk-visually-hidden">Remove updated before filter</span>
×
</a>
</li>
{% endif %}
</ul>
{% endif %}
Rendering the filter summary
When filters are active, display a summary below the filter panel showing the applied filters. Each filter should be a link that removes that specific filter when clicked.
Building the remove filter URL
To remove a filter, you need to rebuild the URL without that specific parameter. Create a Liquid snippet that generates the URL:
{% comment %}
Build URL that removes a specific filter parameter
This snippet creates a URL that preserves all current parameters
except the one being removed. For multi-value parameters (like
checkboxes), it removes only the specific value.
{% endcomment %}
{% comment %}
Example: Remove a single-value filter (e.g., topic)
Current URL: /search?topic=education&status[]=active
Result URL: /search?status[]=active
{% endcomment %}
{% capture remove_topic_url %}{{ page.url }}?{% for param in request.params %}{% unless param[0] == 'topic' %}{{ param[0] }}={{ param[1] | url_encode }}{% unless forloop.last %}&{% endunless %}{% endunless %}{% endfor %}{% endcapture %}
{% comment %}
Example: Remove a specific value from a multi-value filter (e.g., status)
Current URL: /search?status[]=active&status[]=pending
To remove 'active': /search?status[]=pending
This requires rebuilding the parameter without the specific value
{% endcomment %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% assign value_to_remove = 'active' %}
{% capture remove_status_url %}
{{ page.url }}?{% for param in request.params %}{% if param[0] == 'status[]' %}{% comment %} Rebuild status without the removed value {% endcomment %}{% for status in selected_statuses %}{% unless status == value_to_remove %}status[]={{ status | url_encode }}&{% endunless %}{% endfor %}{% else %}{{ param[0] }}={{ param[1] | url_encode }}&{% endif %}{% endfor %}
{% endcapture %}
{% comment %}
Usage in template:
{% endcomment %}
<a href="{{ remove_topic_url | strip }}" class="dfe-filter-summary__tag-remove">
Remove topic filter
</a>
Rendering the filter summary HTML
Use the filter summary component to display active filters:
{% comment %}
Render the filter summary showing active filters
{% endcomment %}
{% assign selected_topic = request.params['topic'] %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% assign type_param = request.params['type[]'] %}
{% assign selected_types = type_param | split: ',' %}
{% comment %} Check if any filters are active {% endcomment %}
{% assign has_filters = false %}
{% if selected_topic != blank %}{% assign has_filters = true %}{% endif %}
{% if selected_statuses.size > 0 %}{% assign has_filters = true %}{% endif %}
{% if selected_types.size > 0 %}{% assign has_filters = true %}{% endif %}
{% if has_filters %}
<div class="dfe-filter-summary">
<h2 class="govuk-heading-s dfe-filter-summary__heading">Filters applied</h2>
<ul class="dfe-filter-summary__list">
{% comment %} Topic filter tag {% endcomment %}
{% if selected_topic != blank %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-label">Topic:</span>
<span class="dfe-filter-summary__tag-value">{{ selected_topic }}</span>
<a href="{{ page.url }}?{% for param in request.params %}{% unless param[0] == 'topic' %}{{ param[0] }}={{ param[1] | url_encode }}&{% endunless %}{% endfor %}"
class="dfe-filter-summary__tag-remove">
<span class="govuk-visually-hidden">Remove topic filter</span>
×
</a>
</li>
{% endif %}
{% comment %} Status filter tags - one for each selected value {% endcomment %}
{% for status in selected_statuses %}
{% capture remove_url %}{{ page.url }}?{% for param in request.params %}{% if param[0] == 'status[]' %}{% for s in selected_statuses %}{% unless s == status %}status[]={{ s | url_encode }}&{% endunless %}{% endfor %}{% else %}{{ param[0] }}={{ param[1] | url_encode }}&{% endif %}{% endfor %}{% endcapture %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-label">Status:</span>
<span class="dfe-filter-summary__tag-value">{{ status }}</span>
<a href="{{ remove_url | strip }}"
class="dfe-filter-summary__tag-remove">
<span class="govuk-visually-hidden">Remove {{ status }} status filter</span>
×
</a>
</li>
{% endfor %}
{% comment %} Type filter tags {% endcomment %}
{% for type in selected_types %}
{% capture remove_url %}{{ page.url }}?{% for param in request.params %}{% if param[0] == 'type[]' %}{% for t in selected_types %}{% unless t == type %}type[]={{ t | url_encode }}&{% endunless %}{% endfor %}{% else %}{{ param[0] }}={{ param[1] | url_encode }}&{% endif %}{% endfor %}{% endcapture %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-label">Type:</span>
<span class="dfe-filter-summary__tag-value">{{ type | replace: '_', ' ' }}</span>
<a href="{{ remove_url | strip }}"
class="dfe-filter-summary__tag-remove">
<span class="govuk-visually-hidden">Remove {{ type }} type filter</span>
×
</a>
</li>
{% endfor %}
</ul>
<a href="{{ page.url }}" class="govuk-link dfe-filter-summary__clear">
Clear all filters
</a>
</div>
{% endif %}
Building the clear all URL
The "Clear all filters" link should remove all filter parameters but preserve non-filter parameters like search keywords:
{% comment %}
Build URL that clears all filter parameters
This preserves non-filter parameters like search keywords
while removing all filter parameters (status, topic, type, order, etc.)
{% endcomment %}
{% comment %}
Define which parameters are filters that should be cleared
{% endcomment %}
{% assign filter_params = 'status[],topic,type[],order,date_from[day],date_from[month],date_from[year],date_to[day],date_to[month],date_to[year]' | split: ',' %}
{% comment %}
Build URL preserving only non-filter parameters (e.g., keywords)
{% endcomment %}
{% capture clear_all_url %}
{{ page.url }}?{% for param in request.params %}{% unless filter_params contains param[0] %}{{ param[0] }}={{ param[1] | url_encode }}&{% endunless %}{% endfor %}
{% endcapture %}
{% comment %}
If you want to clear everything including keywords, simply use the base URL
{% endcomment %}
{% assign clear_everything_url = page.url %}
{% comment %}
Usage in template:
{% endcomment %}
<a href="{{ clear_all_url | strip }}" class="govuk-link dfe-filter-summary__clear">
Clear all filters
</a>
{% comment %}
Or to clear everything including search:
{% endcomment %}
<a href="{{ clear_everything_url }}" class="govuk-link">
Clear all filters and search
</a>
Complete page example
Here's a complete example showing how all the pieces fit together on a search results page:
{% comment %}
Complete search results page with filter panel and results
{% endcomment %}
{% comment %} ===== READ URL PARAMETERS ===== {% endcomment %}
{% assign sort_order = request.params['order'] | default: 'updated-newest' %}
{% assign selected_topic = request.params['topic'] %}
{% assign status_param = request.params['status[]'] %}
{% assign selected_statuses = status_param | split: ',' %}
{% assign keywords = request.params['keywords'] %}
{% comment %} ===== DETERMINE SORT ORDER ===== {% endcomment %}
{% case sort_order %}
{% when 'updated-newest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'true' %}
{% when 'updated-oldest' %}
{% assign sort_attribute = 'modifiedon' %}
{% assign sort_descending = 'false' %}
{% else %}
{% assign sort_attribute = 'createdon' %}
{% assign sort_descending = 'true' %}
{% endcase %}
{% comment %} ===== CHECK IF FILTERS ARE ACTIVE ===== {% endcomment %}
{% assign has_filters = false %}
{% if selected_topic != blank %}{% assign has_filters = true %}{% endif %}
{% if selected_statuses.size > 0 %}{% assign has_filters = true %}{% endif %}
{% comment %} ===== FETCH DATA WITH FILTERS ===== {% endcomment %}
{% fetchxml records %}
<fetch>
<entity name="dfe_record">
<attribute name="dfe_recordid" />
<attribute name="dfe_name" />
<attribute name="dfe_description" />
<attribute name="dfe_topic" />
<attribute name="statuscode" />
<attribute name="modifiedon" />
<order attribute="{{ sort_attribute }}" descending="{{ sort_descending }}" />
{% if has_filters or keywords != blank %}
<filter type="and">
{% if keywords != blank %}
<filter type="or">
<condition attribute="dfe_name" operator="like" value="%{{ keywords }}%" />
<condition attribute="dfe_description" operator="like" value="%{{ keywords }}%" />
</filter>
{% endif %}
{% if selected_topic != blank %}
<condition attribute="dfe_topic" operator="eq" value="{{ selected_topic }}" />
{% endif %}
{% if selected_statuses.size > 0 %}
<filter type="or">
{% for status in selected_statuses %}
<condition attribute="statuscode" operator="eq" value="{{ status }}" />
{% endfor %}
</filter>
{% endif %}
</filter>
{% endif %}
</entity>
</fetch>
{% endfetchxml %}
{% comment %} ===== PAGE TEMPLATE ===== {% endcomment %}
<div class="govuk-grid-row">
<div class="govuk-grid-column-full">
<h1 class="govuk-heading-xl">Search results</h1>
{% comment %} Search input {% endcomment %}
<form method="get" action="{{ page.url }}">
<div class="govuk-form-group">
<label for="keywords" class="govuk-label">Search</label>
<div class="dfe-search-input">
<input type="search"
name="keywords"
id="keywords"
class="govuk-input"
value="{{ keywords }}">
<button type="submit" class="dfe-search-input__button">
Search
</button>
</div>
</div>
{% comment %} ===== FILTER PANEL ===== {% endcomment %}
<div class="dfe-filter-panel" data-module="dfe-filter-panel">
<div class="dfe-filter-panel__header">
<button type="button"
class="dfe-filter-panel__button govuk-link"
aria-expanded="false"
aria-controls="filter-panel-content">
<span class="dfe-filter-panel__button-inner">Filter and sort</span>
</button>
<p class="dfe-filter-panel__count">{{ records.results.entities.size }} results</p>
</div>
<div class="dfe-filter-panel__content" id="filter-panel-content" hidden>
{% comment %} Sort by {% endcomment %}
<details class="dfe-filter-section" open>
<summary class="dfe-filter-section__summary">
<h2 class="dfe-filter-section__summary-heading">Sort by</h2>
</summary>
<div class="dfe-filter-section__content">
<div class="govuk-radios govuk-radios--small">
<div class="govuk-radios__item">
<input type="radio" name="order" id="sort-newest" value="updated-newest"
class="govuk-radios__input"
{% if sort_order == 'updated-newest' %}checked{% endif %}>
<label for="sort-newest" class="govuk-label govuk-radios__label">Updated (newest)</label>
</div>
<div class="govuk-radios__item">
<input type="radio" name="order" id="sort-oldest" value="updated-oldest"
class="govuk-radios__input"
{% if sort_order == 'updated-oldest' %}checked{% endif %}>
<label for="sort-oldest" class="govuk-label govuk-radios__label">Updated (oldest)</label>
</div>
</div>
</div>
</details>
{% comment %} Topic filter {% endcomment %}
<details class="dfe-filter-section">
<summary class="dfe-filter-section__summary">
<h2 class="dfe-filter-section__summary-heading">Topic</h2>
</summary>
<div class="dfe-filter-section__content">
<select name="topic" id="topic" class="govuk-select govuk-!-width-full">
<option value="">All topics</option>
<option value="education" {% if selected_topic == 'education' %}selected{% endif %}>Education</option>
<option value="health" {% if selected_topic == 'health' %}selected{% endif %}>Health</option>
<option value="transport" {% if selected_topic == 'transport' %}selected{% endif %}>Transport</option>
</select>
</div>
</details>
{% comment %} Status filter {% endcomment %}
<details class="dfe-filter-section">
<summary class="dfe-filter-section__summary">
<h2 class="dfe-filter-section__summary-heading">Status</h2>
</summary>
<div class="dfe-filter-section__content">
<div class="govuk-checkboxes govuk-checkboxes--small">
<div class="govuk-checkboxes__item">
<input type="checkbox" name="status[]" id="status-active" value="active"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'active' %}checked{% endif %}>
<label for="status-active" class="govuk-label govuk-checkboxes__label">Active</label>
</div>
<div class="govuk-checkboxes__item">
<input type="checkbox" name="status[]" id="status-pending" value="pending"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'pending' %}checked{% endif %}>
<label for="status-pending" class="govuk-label govuk-checkboxes__label">Pending</label>
</div>
<div class="govuk-checkboxes__item">
<input type="checkbox" name="status[]" id="status-closed" value="closed"
class="govuk-checkboxes__input"
{% if selected_statuses contains 'closed' %}checked{% endif %}>
<label for="status-closed" class="govuk-label govuk-checkboxes__label">Closed</label>
</div>
</div>
</div>
</details>
<div class="dfe-filter-panel__actions">
<button type="submit" class="govuk-button">Apply filters</button>
</div>
</div>
</div>
</form>
{% comment %} ===== FILTER SUMMARY ===== {% endcomment %}
{% if has_filters %}
<div class="dfe-filter-summary">
<h2 class="govuk-heading-s dfe-filter-summary__heading">Filters applied</h2>
<ul class="dfe-filter-summary__list">
{% if selected_topic != blank %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-value">{{ selected_topic }}</span>
<a href="{{ page.url }}?keywords={{ keywords | url_encode }}{% for status in selected_statuses %}&status[]={{ status | url_encode }}{% endfor %}&order={{ sort_order }}"
class="dfe-filter-summary__tag-remove">×</a>
</li>
{% endif %}
{% for status in selected_statuses %}
<li class="dfe-filter-summary__tag">
<span class="dfe-filter-summary__tag-value">{{ status }}</span>
<a href="{{ page.url }}?keywords={{ keywords | url_encode }}&topic={{ selected_topic | url_encode }}{% for s in selected_statuses %}{% unless s == status %}&status[]={{ s | url_encode }}{% endunless %}{% endfor %}&order={{ sort_order }}"
class="dfe-filter-summary__tag-remove">×</a>
</li>
{% endfor %}
</ul>
<a href="{{ page.url }}?keywords={{ keywords | url_encode }}" class="govuk-link">Clear all filters</a>
</div>
{% endif %}
{% comment %} ===== RESULTS ===== {% endcomment %}
<ul class="govuk-list">
{% for record in records.results.entities %}
<li class="dfe-search-result">
<h2 class="govuk-heading-m">
<a href="/record/{{ record.dfe_recordid }}" class="govuk-link">{{ record.dfe_name }}</a>
</h2>
<p class="govuk-body">{{ record.dfe_description }}</p>
<p class="govuk-body-s govuk-!-margin-bottom-0">
Updated: {{ record.modifiedon | date: '%d %B %Y' }}
</p>
</li>
{% endfor %}
</ul>
{% if records.results.entities.size == 0 %}
<p class="govuk-body">No results found. Try adjusting your filters.</p>
{% endif %}
</div>
</div>
JavaScript for the design manual
The filter panel component in this design manual includes JavaScript that demonstrates the filter behaviour. This JavaScript handles:
- reading URL parameters on page load and pre-selecting form inputs
- showing selected filters in real-time as users make selections
- building URL parameters and reloading the page when "Apply" is clicked
- removing individual filters and reloading with updated parameters
- clearing all filters and reloading
For Power Pages, you typically won't need this JavaScript as the filtering is handled server-side with Liquid. The JavaScript is provided for demonstration purposes in the design manual and for scenarios where you want to show filter selections before applying.
The filter panel component JavaScript automatically provides real-time selection feedback, showing users their selected filters before they click Apply.
Best practices
- Always wrap the filter panel in a form with
method="get"so filter values appear in the URL - Preserve non-filter parameters (like search keywords) when building filter URLs
- Use meaningful parameter names that describe the filter (e.g.
statusnots) - Provide a "Clear all filters" option when multiple filters are active
- Show a count of results to help users understand the effect of their filters
- Consider pagination alongside filtering for large result sets
- Test with no filters, single filters, and multiple filters to ensure all combinations work