Glossary with Alphabet Navigation
This glossary implementation uses Loops & Logic to create a dynamic A-Z navigation for browsing terminology.
– The template first loops through all glossary posts to check if any entries start with numbers, then builds a complete alphabet list (with “#” added when numeric entries exist).
– Next, it creates a list of actually available letters by examining the first character of each post title, converting it to uppercase, and storing unique values.
– With these lists prepared, the template renders a container with Schema.org markup, a search box with a status indicator, and two navigation options – a horizontal A-Z list for desktop and a dropdown for mobile.
– For each available letter, the template loops through posts again to group entries under the appropriate letter headings, using conditional logic to handle special cases like numeric entries.
The result is a fully responsive glossary where L&L’s logic handles all the filtering and content organization before any JavaScript runs on the page.
Preview
No matching glossary terms found. Please try a different search.
#
- 4 Reasons your Shortcodes are on the Way Out
A
- An absolute beginner's guide to Loops & Logic
C
- Creating CCS-like shortcodes in Loops & Logic
- Custom Content Shortcode to Loops & Logic Transition - FAQs
E
- Everything you need to know about the Loop tag
G
- Getting started with L&L terminology and syntax
U
- Update: animated pagination, math variable, and more in version 2.4.4
- Update: editor upgrade, Pods support, and new export format in versions 3.3.0 to 3.3.1
- Update: insert headers/footers, RegEx, and more in versions 3.2.0 to 3.2.9
- Update: make your templates more responsive with the new device variable in Loops & Logic version 2.4.3
- Update: multiple comparisons in a single If tag, new modulo operator, and more in Loops & Logic Version 2.4.1
- Update: new builder integration and improvements in versions 3.1.0 to 3.1.9
- Update: new docs, restructured plugins, and more in version 3.0.0
- Update: template previews and new code editor in versions 4.0.0 to 4.0.2
- Update: Theme locations and more with Loops & Logic Version 1.2.0
- Update: Theme PHP Templates in Layouts and more in Loops & Logic Version 1.2.3
W
- Why don't you just learn PHP?
<Note>First, check if there are any numeric entries</Note>
<Set has_numeric_entries>false</Set>
<Loop type=post post_type=glossary orderby=title order=asc>
<Set first_char><Format length=1><Field title /></Format></Set>
<If check="{Get first_char}" matches_pattern="/[0-9]/">
<Set has_numeric_entries>true</Set>
</If>
</Loop>
<Note>Create alphabet list, conditionally including # for numbers</Note>
<Set alphabet>
<List>
<If check="{Get has_numeric_entries}" is value="true">
<Item>#</Item>
</If>
<Item>A</Item><Item>B</Item><Item>C</Item><Item>D</Item><Item>E</Item><Item>F</Item>
<Item>G</Item><Item>H</Item><Item>I</Item><Item>J</Item><Item>K</Item><Item>L</Item>
<Item>M</Item><Item>N</Item><Item>O</Item><Item>P</Item><Item>Q</Item><Item>R</Item>
<Item>S</Item><Item>T</Item><Item>U</Item><Item>V</Item><Item>W</Item><Item>X</Item>
<Item>Y</Item><Item>Z</Item>
</List>
</Set>
<Note>Build list of available letters</Note>
<Set available_letters>
<List>
</List>
</Set>
<Note>Populate available letters list</Note>
<Loop type=post post_type=glossary orderby=title order=asc>
<Set first_char><Format length=1><Field title /></Format></Set>
<Set first_letter><Format case=upper><Get first_char /></Format></Set>
<Note>Check if this is a number and use #</Note>
<If check="{Get first_char}" matches_pattern="/[0-9]/">
<Set first_letter>#</Set>
</If>
<Set already_added>false</Set>
<Note>Check if letter already in our list</Note>
<Loop list="{Get available_letters}">
<If check="{Field /}" is value="{Get first_letter}">
<Set already_added>true</Set>
</If>
</Loop>
<Note>If not already in list, add it</Note>
<If check="{Get already_added}" is value="false">
<Set available_letters>
<List>
<Loop list="{Get available_letters}">
<Item><Field /></Item>
</Loop>
<Item><Get first_letter /></Item>
</List>
</Set>
</If>
</Loop>
<Note>Schema.org markup for glossary</Note>
<JSON-LD>
{
"@context": "https://schema.org",
"@type": "DefinedTermSet",
"name": "Glossary",
"description": "Complete glossary of terms"
}
</JSON-LD>
<div class="glossary-container" itemscope itemtype="https://schema.org/DefinedTermSet">
<!-- Search Box -->
<div class="glossary-search-container">
<div class="search-field-wrapper">
<label for="glossary-search" class="sr-only">Search glossary terms</label>
<input type="text" id="glossary-search" class="glossary-search-input" placeholder="Search glossary terms..." aria-label="Search glossary terms">
<button type="button" id="glossary-search-btn" class="glossary-search-button">Search</button>
</div>
<div id="search-status" class="search-status" aria-live="polite"></div>
</div>
<!-- Desktop A-Z Navigation -->
<nav class="alphabet-nav" aria-label="Glossary alphabetical navigation">
<ul class="alphabet-list">
<Loop list="{Get alphabet}">
<Set current_letter><Field /></Set>
<li>
<Set is_available>false</Set>
<Note>Check if this letter has entries</Note>
<Loop list="{Get available_letters}">
<If check="{Field /}" is value="{Get current_letter}">
<Set is_available>true</Set>
</If>
</Loop>
<If check="{Get is_available}" is value="true">
<a href="#{Get current_letter}" class="alphabet-link active">
<Get current_letter />
</a>
<Else />
<span class="alphabet-link disabled" aria-disabled="true">
<Get current_letter />
</span>
</If>
</li>
</Loop>
</ul>
</nav>
<!-- Mobile Dropdown Navigation -->
<div class="mobile-alphabet-nav">
<label for="letter-select" class="sr-only">Jump to letter</label>
<select id="letter-select" onChange="if(this.value) window.location.hash = this.value;" aria-label="Select a letter to jump to that section">
<option value="">Jump to Letter</option>
<Loop list="{Get alphabet}">
<Set current_letter><Field /></Set>
<Set is_available>false</Set>
<Note>Check if this letter has entries</Note>
<Loop list="{Get available_letters}">
<If check="{Field /}" is value="{Get current_letter}">
<Set is_available>true</Set>
</If>
</Loop>
<If check="{Get is_available}" is value="true">
<option value="#{Get current_letter}"><Get current_letter /></option>
<Else />
<option value="" disabled><Get current_letter /> (No entries)</option>
</If>
</Loop>
</select>
</div>
<!-- Main glossary content -->
<div class="glossary-content">
<!-- No results message -->
<div id="no-results" class="no-results">
<p>No matching glossary terms found. Please try a different search.</p>
</div>
<Note>Loop through available letters and create sections</Note>
<Loop list="{Get available_letters}">
<Set current_letter><Field /></Set>
<section id="{Get current_letter}" class="letter-section">
<h2 class="letter-heading"><Get current_letter /></h2>
<dl class="glossary-terms">
<Note>Loop through glossary posts for this letter</Note>
<Loop type=post post_type=glossary orderby=title order=asc>
<Set first_char><Format length=1><Field title /></Format></Set>
<Set post_first_letter><Format case=upper><Get first_char /></Format></Set>
<Note>If this is a number and we're in the # section</Note>
<If check="{Get current_letter}" is value="#">
<If check="{Get first_char}" matches_pattern="/[0-9]/">
<dt itemprop="name" id="term-{Field id}" class="glossary-term" data-term="{Field title}"><Field title /></dt>
<dd itemprop="description" class="glossary-def"><Field glossary_description /></dd>
</If>
<Else />
<Note>Regular letter matching</Note>
<If check="{Get post_first_letter}" is value="{Get current_letter}">
<dt itemprop="name" id="term-{Field id}" class="glossary-term" data-term="{Field title}"><Field title /></dt>
<dd itemprop="description" class="glossary-def"><Field glossary_description /></dd>
</If>
</If>
</Loop>
</dl>
</section>
</Loop>
</div>
</div>
jQuery(document).ready(function($) {
// Initialize variables
const $searchInput = $("#glossary-search");
const $searchButton = $("#glossary-search-btn");
const $searchStatus = $("#search-status");
const $noResults = $("#no-results");
const $glossaryTerms = $(".glossary-term");
const $glossaryDefs = $(".glossary-def");
const $sections = $(".letter-section");
let matchedTerms = [];
let searchDelay;
// Function to handle hash navigation
function scrollToSection() {
if (window.location.hash) {
const $targetSection = $(window.location.hash);
if ($targetSection.length) {
setTimeout(function() {
const headerOffset = 100;
$("html, body").animate({
scrollTop: $targetSection.offset().top - headerOffset
}, 500);
}, 10);
}
}
}
// Simple search function
function doSearch() {
const searchTerm = $searchInput.val().trim().toLowerCase();
// Reset previous search
resetSearch();
// Empty search shows everything
if (!searchTerm) {
$searchStatus.text("");
$noResults.hide();
$("#glossary-clear-btn").hide();
updateUrlWithSearch("");
return;
}
// Show clear button
$("#glossary-clear-btn").show();
// Update URL
updateUrlWithSearch(searchTerm);
// Variables to track results
matchedTerms = [];
let matchCount = 0;
const visibleSections = {};
// Check each term and definition
$glossaryTerms.each(function(index) {
const $term = $(this);
const termText = $term.text().toLowerCase();
const $def = $glossaryDefs.eq(index);
const defText = $def.length ? $def.text().toLowerCase() : "";
const $section = $term.closest("section");
// Check both term and definition text for matches
if (termText.includes(searchTerm) || defText.includes(searchTerm)) {
// Make this term visible and track it
$term.removeClass("filtered-out");
if ($def.length) $def.removeClass("filtered-out");
matchedTerms.push($term[0]);
matchCount++;
// Track which section has visible items
if ($section.length && $section.attr("id")) {
visibleSections[$section.attr("id")] = true;
}
// Highlight term if it contains the search term
if (termText.includes(searchTerm)) {
const originalText = $term.attr("data-term");
let html = "";
const lowerText = originalText.toLowerCase();
let searchPos = lowerText.indexOf(searchTerm);
if (searchPos >= 0) {
html = originalText.substring(0, searchPos) +
"<span class=\"highlight\">" +
originalText.substring(searchPos, searchPos + searchTerm.length) +
"</span>" +
originalText.substring(searchPos + searchTerm.length);
$term.html(html);
}
}
// Add definition highlighting
if ($def.length && defText.includes(searchTerm)) {
// Save original definition content if we haven't already
if (!$def.attr("data-original")) {
$def.attr("data-original", $def.html());
}
// Simple text highlighting for definition
const defHtml = $def.attr("data-original");
const defLower = defText.toLowerCase();
// Find and highlight all occurrences
let lastIndex = 0;
let newHtml = "";
let currentPos = 0;
while ((currentPos = defLower.indexOf(searchTerm, lastIndex)) > -1) {
// Add content before match
newHtml += defHtml.substring(lastIndex, currentPos);
// Add highlighted match
newHtml += "<span class=\"highlight\">";
newHtml += defHtml.substring(currentPos, currentPos + searchTerm.length);
newHtml += "</span>";
// Move position
lastIndex = currentPos + searchTerm.length;
}
// Add remaining content
newHtml += defHtml.substring(lastIndex);
$def.html(newHtml);
}
} else {
// Hide non-matching terms
$term.addClass("filtered-out");
if ($def.length) $def.addClass("filtered-out");
}
});
// Show/hide sections based on matches
$sections.each(function() {
const $section = $(this);
if (visibleSections[$section.attr("id")]) {
$section.removeClass("filtered-out");
} else {
$section.addClass("filtered-out");
}
});
// Update status message
if (matchCount > 0) {
$searchStatus.text("Found " + matchCount + (matchCount === 1 ? " matching term" : " matching terms"));
$noResults.hide();
} else {
$searchStatus.text("No matching terms found");
$noResults.show();
}
}
// Reset search function
function resetSearch() {
// Reset terms and definitions
$glossaryTerms.each(function(index) {
const $term = $(this);
const $def = $glossaryDefs.eq(index);
$term.removeClass("filtered-out");
if ($def.length) {
$def.removeClass("filtered-out");
// Reset definition to original content if stored
if ($def.attr("data-original")) {
$def.html($def.attr("data-original"));
}
}
// Remove highlighting by resetting to original text
const originalText = $term.attr("data-term");
$term.text(originalText);
});
// Reset sections
$sections.removeClass("filtered-out");
// Clear search status message
$searchStatus.text('');
// Hide no results message
$noResults.hide();
}
// Scroll to first match
function scrollToFirstMatch() {
if (matchedTerms.length > 0) {
const $firstMatch = $(matchedTerms[0]);
const headerOffset = 120;
$("html, body").animate({
scrollTop: $firstMatch.offset().top - headerOffset
}, 500);
}
}
// Update URL with search query
function updateUrlWithSearch(searchTerm) {
if (searchTerm) {
const newUrl = new URL(window.location);
newUrl.searchParams.set("search", searchTerm);
window.history.replaceState({}, "", newUrl);
} else {
const newUrl = new URL(window.location);
newUrl.searchParams.delete("search");
window.history.replaceState({}, "", newUrl);
}
}
// Check for search parameter on page load
function checkForSearchParam() {
const urlParams = new URLSearchParams(window.location.search);
const searchParam = urlParams.get("search");
if (searchParam) {
$searchInput.val(searchParam);
doSearch();
scrollToFirstMatch();
}
}
// Highlight current letter on scroll
function highlightCurrentLetterOnScroll() {
const scrollPosition = $(window).scrollTop() + 150;
// Find the current section
$sections.each(function() {
const $section = $(this);
if ($section.is(":visible")) {
const sectionTop = $section.offset().top;
const sectionBottom = sectionTop + $section.outerHeight();
if (scrollPosition >= sectionTop && scrollPosition < sectionBottom) {
const currentLetter = $section.attr("id");
// Remove current class from all links
$(".alphabet-link").parent().removeClass("current");
// Add current class to current letter
$(`.alphabet-list li:has(a.alphabet-link:contains("${currentLetter}"))`).addClass("current");
return false; // Exit the each loop
}
}
});
}
// Add Clear button
$("<button>", {
type: "button",
id: "glossary-clear-btn",
class: "glossary-clear-button",
text: "× Clear"
}).appendTo(".search-field-wrapper").hide().on("click", function() {
$searchInput.val("");
resetSearch();
updateUrlWithSearch("");
$(this).hide();
});
// Event listeners
$searchInput.on("input", function() {
clearTimeout(searchDelay);
searchDelay = setTimeout(doSearch, 300);
});
$searchButton.on("click", function() {
doSearch();
scrollToFirstMatch();
});
$searchInput.on("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
doSearch();
scrollToFirstMatch();
}
});
// Mobile dropdown
$("#letter-select").on("change", function() {
if ($(this).val()) {
window.location.hash = $(this).val();
}
});
// Throttle function for scroll event
function throttle(func, wait) {
let lastTime = 0;
return function() {
const now = new Date().getTime();
if (now - lastTime >= wait) {
func.apply(null, arguments);
lastTime = now;
}
};
}
// Scroll event for highlighting current letter
$(window).on("scroll", throttle(highlightCurrentLetterOnScroll, 100));
// Initialize
scrollToSection();
checkForSearchParam();
$(window).on("hashchange", scrollToSection);
});
:root {
--color-primary: #007bff;
--color-primary-hover: #0056b3;
--color-text: #333;
--color-text-muted: #666;
--color-background: #fff;
--color-background-light: #f5f5f5;
--color-background-highlight: #ffff88;
--color-border: #ddd;
}
/* Search box */
.search-field-wrapper {
display: flex;
gap: 10px;
max-width: 600px;
margin-bottom: 10px;
}
.glossary-search-input {
flex: 1;
padding: 12px 16px;
border: 1px solid var(--color-border);
border-radius: 4px;
font-size: 16px;
}
.glossary-search-button {
padding: 0 20px;
background-color: var(--color-primary);
color: var(--color-background);
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.glossary-search-button:hover {
background-color: var(--color-primary-hover);
}
.glossary-clear-button {
padding: 0 20px;
background-color: var(--color-text-muted);
color: var(--color-background);
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.2s;
}
.glossary-clear-button:hover {
background-color: var(--color-text);
}
.search-status {
height: 24px;
font-size: 14px;
color: var(--color-text-muted);
}
/* Highlight search matches */
.highlight {
background-color: var(--color-background-highlight);
}
/* No results message */
.no-results {
padding: 20px;
background-color: var(--color-background-light);
border-radius: 4px;
text-align: center;
margin-top: 20px;
display: none;
}
/* Desktop alphabet navigation */
.alphabet-nav {
position: sticky;
top: 20px;
z-index: 100;
margin-bottom: 30px;
background-color: var(--color-background);
padding: 8px 0;
}
.admin-bar .alphabet-nav{
top: 52px
}
.alphabet-list {
display: flex;
flex-wrap: wrap;
list-style: none;
padding: 0;
margin: 0;
gap: 8px;
}
.alphabet-link {
display: inline-block;
width: 36px;
height: 36px;
line-height: 36px;
text-align: center;
border-radius: 50%;
font-weight: bold;
transition: all 0.2s ease;
}
.alphabet-link.active {
background-color: var(--color-background-light);
color: var(--color-text);
text-decoration: none;
cursor: pointer;
}
.alphabet-link.disabled {
background-color: var(--color-background-light);
color: var(--color-text);
cursor: default;
opacity: 0.4;
}
.alphabet-link.active:hover, .alphabet-link.active:focus {
background-color: var(--color-primary);
color: var(--color-background);
}
/* Current letter highlight */
.alphabet-list li.current .alphabet-link.active {
background-color: var(--color-primary);
color: var(--color-background);
}
/* Mobile dropdown navigation */
.mobile-alphabet-nav {
display: none;
margin-bottom: 30px;
position: sticky;
top: 20px;
z-index: 100;
background-color: var(--color-background);
padding: 8px 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
#letter-select {
width: 100%;
padding: 12px;
border-radius: 4px;
border: 1px solid var(--color-border);
font-size: 16px;
}
/* Letter sections */
.letter-heading {
font-size: 2rem;
border-bottom: 2px solid var(--color-primary);
padding-bottom: 10px;
margin-top: 50px;
margin-bottom: 20px;
color: var(--color-primary);
}
/* Glossary terms */
.glossary-terms {
margin: 0;
}
.glossary-terms dt {
font-weight: bold;
font-size: 1.2rem;
margin-top: 20px;
margin-bottom: 5px;
}
.glossary-terms dd {
margin-left: 0;
margin-bottom: 20px;
line-height: 1.5;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* Filtered styles */
.filtered-out {
display: none !important;
}
/* Responsive */
@media (max-width: 768px) {
.alphabet-nav {
display: none;
}
.mobile-alphabet-nav {
display: block;
}
.letter-heading {
font-size: 1.75rem;
}
}