Live Search widget
The complete guide to the WidgetCore Live Search widget — General, Layout, Query and Pagination controls, result structure, styles, JavaScript configuration and how to send custom queries.
The Live Search widget sends the visitor's query to WordPress' REST API and renders the results without a page reload. Everything the widget does is built on top of a real search form, so if JavaScript is unavailable the form still submits and the theme's native search results page opens.
| Item | Value |
|---|---|
| Widget name | wgcr-search |
| Panel title | Live Search |
| Category | WidgetCore Widgets (wgcr) |
| Style / script | wgcr-search |
| Root class | .wgcr-search |
| REST route | /wp-json/wgcr/v1/search |
General section
| Control | Type | Default | Notes |
|---|---|---|---|
| Results display mode | Select | List mode | List mode (dropdown panel) or Page mode (always-visible region) |
| Placeholder text | Text | Empty | Filled automatically: “Search in posts…”, or a post-type aware text such as “Search in courses…” |
| Show thumbnail | Switcher | Yes | Featured image next to each result |
| Show excerpt | Switcher | Yes | 18 words with an ellipsis |
| “Show all” text | Text | “Show all results” | Label of the link to the site search page |
Results display mode is the most important choice; it changes the whole experience and the list of available controls. See the display modes page.
Display modes
The live search widget has two display modes, and this single control decides where results are shown, which extra controls are available and how visitors navigate them.
Comparing the two modes
| Feature | List mode (modal) | Page mode (page) |
|---|---|---|
| Where results appear | A dropdown panel below the field | An always-visible region inside the same widget |
| Initial panel state | Closed with hidden | Open (aria-expanded="true" on the field) |
| Closes on an outside click | Yes | No (the panel is part of the page) |
Escape behaviour | Closes the panel; if already closed, clears the query | Clears the search query |
| Columns | Always a single column | 1 to 6 columns (responsive) |
| Masonry and equal height | Not available | Available |
| Results container styling | Panel background and radius only | Width, minimum/maximum height, spacing, background, alignment |
| Infinite scroll | Inside the panel (panel scroll) | Relative to the browser window |
| “Show all” link | Shown after the results | Only when the query is valid |
| Good fit for | Header, sidebar, quick search | A dedicated search page, an archive, a course section |
List mode
The default mode. The panel opens after the visitor types two or more characters and closes on an outside click, with Escape, or when the widget loses focus.
[ Search field ▾ ]
└──────────────────────────────┐
│ 8 results All text │
│ ┌────┐ Result title │
│ │ img│ Excerpt… │
│ └────┘ 2026-09-01 │
│ ┌────┐ Result title │
└──────────────────────────────┘
| Feature | Behaviour |
|---|---|
| Opening | After 2 characters are typed (250 ms debounce) |
| Closing | Outside click, Escape, or selecting a result |
| Max height | 70vh (adjustable in the “Results box” section) |
| Columns | Always 1 |
| Masonry | Not available |
| Pagination | Numbers, previous/next, load on click, load on scroll |
The visitor clicks the field
If a valid query is already in the field and results are still loaded, the panel opens.
The visitor types
After 250 milliseconds the request is sent and the panel opens.
A result is picked with the keyboard or the mouse
The up and down arrows move between results and
Enteropens the selected one.The panel closes with an outside click or
EscapeA second
Escapeclears the text in the field.
Page mode
The results region is always visible inside the widget, so it behaves like a real search page: the visitor types a query and results appear below in the same view.
| Feature | Behaviour |
|---|---|
| Results visibility | Always visible (no outside click to close) |
| Layout controls | Template choice, items per page, columns, masonry, equal height |
| Columns | 1 to 6, responsive (defaults 3 / 2 / 1) |
| Masonry | Available |
| Equal height | Available |
| Template | Default or a custom Elementor template |
| Pagination | Inside the same region, below the results |
Columns, masonry and equal height
| Setting | Effect |
|---|---|
| Columns | The wgcr-search--cols-{n} class on the root; with 3 or more, a CSS grid |
| Masonry | The wgcr-search--masonry class; items are positioned by their natural height |
| Equal height | The wgcr-search--equal class; all cards stretch to the tallest one |
Choosing a template in Page mode
When “Template choice” is set to Custom, the widget sends the template id to the REST route and each result is rendered with your Elementor template:
Build the template in the library
“Templates → Saved Templates”; the template must be published.
Select it in the widget
“Template choice” → Custom, then pick the template.
Check the output
The root gets the
wgcr-search--tplclass and the template's own CSS loads once.
Details on the Elementor templates page.
Behaviour differences that are easy to miss
Selecting with the mouse
- List mode: moving the mouse over a result marks it as “selected” so
Enteropens it. - Both modes with an Elementor template: when results are rendered by a custom template (
wgcr-search-item--tpl), hovering no longer marks a result as selected and the selected styling is not applied — the template has its own links and styles, and overlapping them would break clicks. If a templated result has no link of its own, clicking the card opens that result.
Focus after pagination
When you change page with the keyboard, focus returns to the button you pressed (previous, next, or the current page number) after the new results are rendered. Keyboard users therefore never lose their place in the pagination bar.
Infinite scroll
In Page mode a sentinel element (.wgcr-search-sentinel) measures the end of the results with IntersectionObserver against the browser window, so loading more does not require scrolling inside a panel. In List mode the panel's own scroll is what counts.
Accessibility in both modes
| Feature | List mode | Page mode |
|---|---|---|
role="combobox" on the field | Yes | Yes |
aria-expanded and aria-controls | Yes | Yes |
role="listbox" on the results panel | Yes | Yes |
aria-live="polite" for status messages | Yes | Yes |
Result navigation with ArrowDown / ArrowUp | Yes | Yes |
Opening a result with Enter | Yes | Yes |
Closing with Escape | Yes | — (there is nothing to close) |
Full details on the accessibility page and the keyboard shortcuts page.
Style differences
| Area | List mode | Page mode |
|---|---|---|
| Root class | wgcr-search--modal | wgcr-search--page |
| Results background | The dropdown panel background | The widget container background |
| Max height | 70vh | Unlimited (follows the container) |
| Grid columns | 1 | 1 to 6 |
Because both modes share the same root class (.wgcr-search), you can write one CSS rule for the shared parts and add mode-specific rules with the modifier classes.
Which mode to choose
| Your need | Mode |
|---|---|
| A compact search field in the header or sidebar | List mode |
| A search page with a grid and pagination | Page mode |
| Showing results with a custom card design | Page mode + custom template |
| Infinite scroll while browsing | Page mode + “Load on scroll” |
| A search form inside an Elementor page that looks like a panel | List mode |
| Scenario | Settings |
|---|---|
| Quick search in the header | List mode, 5 results, no thumbnail, pagination “None” |
| The main search page | Page mode, 12 results, 3 columns, numbered pagination |
| A course grid | Page mode, source course, columns 3/2/1, masonry on, load on scroll |
| Results with a bespoke design | Page mode, custom template, 2 columns, previous/next pagination |
Elementor templates
Besides the built-in card, the live search can render each result with an Elementor template you designed yourself. This gives you full control over the result markup while the plugin keeps handling the query, pagination and accessibility.
| Template choice | Value | Markup |
|---|---|---|
| Default template | default | The plugin's card: thumbnail, title, excerpt, date |
| Custom | custom | Your Elementor template, rendered per result |
Preparing the template
Create a template in the library
“Templates → Saved Templates → Add New”, type “Section”.
Design one result card
Use Dynamic Tags for the post fields; the template is rendered in the context of each result.
Publish the template
An unpublished template cannot be selected.
Select it in the widget
“Template choice” → Custom, then pick the template from the list.
What happens during a search
When a template is selected, the request carries the template id:
GET /wp-json/wgcr/v1/search?q=course&limit=12&type=post&template=42
The server renders the template for every matching post:
$plugin = \Elementor\Plugin::instance();
$document = $plugin->documents->get( $template_id );
$html = $plugin->frontend->get_builder_content_for_display( $template_id );
The response then includes an html field per result alongside id, title, link, thumb, excerpt and date. The client inserts that HTML directly, which is why no escaping happens twice.
Checking for visible content
Before the rendered template is used, the plugin strips tags and checks whether anything visible is left:
$visible = trim( wp_strip_all_tags( $html ) );
If the result is empty — because the template has no content, or its conditions do not match — the plugin falls back to the default card. This prevents the results area from turning into a blank block.
How the template CSS is loaded
Elementor prints each template's CSS with enqueue_css. Since search results are injected after the page has loaded, the plugin takes two steps:
- Inline critical CSS:
post_cssfor the template is printed inline once per page, so the injected markup is styled immediately. - Register the template style handle:
elementor-post-{id}.cssis added to the widget's style dependencies so it loads on pages that contain the widget.
| Situation | Result |
|---|---|
| First search on the page | The template CSS is already present (inline) |
| Subsequent searches | No extra request |
| A template with external fonts or icons | They are loaded by Elementor as usual |
Entry animations and handlers
Elementor elements often carry animations or interactions (hover effects, counters, carousels). Because the markup is injected after page load, those handlers are not bound automatically. The plugin therefore runs:
if ( window.elementorFrontend ) {
window.elementorFrontend.elementsHandler.runReadyTrigger( container );
}
on the injected results container, so animations and interactions defined in the template work as if the content had been printed server-side.
Templated results and interaction
| Feature | Default card | Custom template |
|---|---|---|
| Root class | — | wgcr-search--tpl |
role="option" on each result | Yes | Yes |
Keyboard navigation (ArrowDown/ArrowUp) | Yes | Yes |
Opening with Enter | Yes | Yes (the first link inside the result) |
| Thumbnail control | Applies | Does not apply (your template decides) |
| Excerpt control | Applies | Does not apply |
Recommended template pattern
┌──────────────────────────────────────────┐
│ ┌──────┐ Title (H4 or H3) │
│ │ thumb│ Excerpt (2 lines, clamp) │
│ └──────┘ Category • Date │
└──────────────────────────────────────────┘
| Element | Suggested setting |
|---|---|
| Title | Heading, linked to the post URL, size H4 |
| Thumbnail | Featured image, square, object-fit: cover |
| Excerpt | Text, 18 words with an ellipsis |
| Meta | Post info (category, date) in small text |
| Card | Background, radius and a soft shadow |
| Height | “Equal height” in the widget for a tidy grid |
Template troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Results appear as the default card | The template rendered no visible content | Check the template content and its display conditions |
| The template is not in the list | It is not published, or it is the wrong type | Publish it as a “Section” template |
| The template appears unstyled | Its CSS did not load | Clear caches; check that elementor-post-{id}.css is requested |
| Animations do not run | elementorFrontend is not available | Check that Elementor's scripts load on the page |
| Layout breaks in a grid | The template has its own width | Remove fixed widths inside the template and let the widget grid handle sizing |
Query and filters
Every search in the plugin ends in one WP_Query. This page shows what goes into that query, what you can control without writing code, and the two filters that let you change the query on the server.
Base query arguments
| Argument | Value | Notes |
|---|---|---|
s | The typed query | WordPress' own search matching |
post_type | The sanitised source | Invalid values become post |
post_status | publish | Drafts and private posts never appear |
has_password | false | Password-protected posts are excluded |
posts_per_page | The requested limit, clamped to 1–50 | — |
paged | The requested page | Only when greater than 0 |
orderby / order | Per the controls | With relevance, no orderby is added |
ignore_sticky_posts | Per the ignore_sticky value | — |
tax_query | Built from terms and terms_op | Only when terms are given |
date_query | Built from date | Only when not all |
Filters without code
The tax_query structure
With terms_op="include" several terms are combined with an OR relation:
'tax_query' => array(
'relation' => 'OR',
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 12, 18 ),
'operator' => 'IN',
),
),
With terms_op="exclude" a single entry excludes the terms:
'tax_query' => array(
array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 12, 18 ),
'operator' => 'NOT IN',
),
),
The terms format
terms="12,18" → category:12, category:18
terms="category:12,post_tag:7" → mixed taxonomies
terms="product_cat:33" → a WooCommerce category
Each item is split on the colon; when the taxonomy part is missing, category is assumed. Ids are cast to positive integers and anything that is not a number is dropped.
Server-side filters
Two filters let you change the final arguments:
| Filter | Arguments | When it runs |
|---|---|---|
wgcr_search_query_args | $args, $query_id, $request | For every search request |
wgcr_search_query_args/{query_id} | $args, $request | Only for a widget or shortcode with that Query ID |
The general filter runs first, so the id-specific one always has the final say.
Where to write them
/* A small custom plugin, or the theme's functions.php */
add_filter( 'wgcr_search_query_args', 'my_search_args', 10, 3 );
function my_search_args( $args, $query_id, $request ) {
/* your changes */
return $args;
}
Practical examples
Search several post types at once
add_filter( 'wgcr_search_query_args', function ( $args ) {
$args['post_type'] = array( 'post', 'page', 'course' );
return $args;
} );
Restrict one widget by Query ID
add_filter( 'wgcr_search_query_args/featured', function ( $args, $request ) {
$args['meta_query'] = array(
array( 'key' => 'featured', 'value' => 'yes' ),
);
return $args;
}, 10, 2 );
Combine terms with AND instead of OR
add_filter( 'wgcr_search_query_args', function ( $args ) {
if ( ! empty( $args['tax_query'] ) && 'OR' === ( $args['tax_query']['relation'] ?? '' ) ) {
$args['tax_query']['relation'] = 'AND';
}
return $args;
} );
Search post titles only
add_filter( 'posts_search', function ( $search, $query ) {
if ( ! empty( $query->get( 'wgcr_title_only' ) ) ) {
global $wpdb;
$term = '%' . $wpdb->esc_like( $query->get( 's' ) ) . '%';
$search = " AND {$wpdb->posts}.post_title LIKE '{$term}' ";
}
return $search;
}, 10, 2 );
add_filter( 'wgcr_search_query_args', function ( $args, $query_id ) {
if ( 'titles' === $query_id ) {
$args['wgcr_title_only'] = 1;
}
return $args;
}, 10, 2 );
Exclude a category site-wide
add_filter( 'wgcr_search_query_args', function ( $args ) {
$args['tax_query'][] = array(
'taxonomy' => 'category',
'field' => 'term_id',
'terms' => array( 99 ),
'operator' => 'NOT IN',
);
return $args;
} );
Performance notes
| Situation | Recommendation |
|---|---|
| A large site with many posts | Keep limit at 12 or less and avoid orderby=rand |
A meta_query on an unindexed meta key | Add a database index for that key |
A heavy tax_query with many terms | Narrow the term list; NOT IN on a large set is slow |
| Repeated popular queries | Cache the route response for anonymous visitors |
Random ordering (rand) | Costs a full sort on every request; use it sparingly |
Pagination
Pagination only matters when the number of matches is larger than “Items per page”. The widget offers four types, and each one uses a different mechanism: replacing the current results or appending new ones.
| Type | Control value | Behaviour | Results |
|---|---|---|---|
| None | none | Only the first page of results | — |
| Numbers | numbers | A page bar with previous/next and “Page X of Y” | Replaced |
| Previous / next | prev_next | Two buttons | Replaced |
| Load on click | load_on_click | A “Load more” button | Appended |
| Load on scroll | load_on_scroll | Automatic near the end of the list | Appended |
The four pagination types
Numbers is the most complete type: a bar with page buttons, previous and next links and an accessible “Page X of Y” text. Because it needs the total number of results, the request carries page and the server returns the X-WP-Total and X-WP-TotalPages headers.
Previous and next uses the same mechanism but only renders two buttons, which suits narrow layouts.
Load on click appends the next page to the existing list with a button click. The visitor keeps their scroll position and can go back to earlier results without a request.
Load on scroll replaces the button with an IntersectionObserver: when the sentinel element comes within about 300 pixels of the bottom of the viewport, the next page loads automatically.
The page bar markup
<nav class="wgcr-search-pager" aria-label="Pagination">
<button type="button" class="wgcr-search-pg wgcr-search-pg--prev">Previous</button>
<button type="button" class="wgcr-search-pg is-active" aria-current="page">1</button>
<button type="button" class="wgcr-search-pg">2</button>
<button type="button" class="wgcr-search-pg">3</button>
<button type="button" class="wgcr-search-pg wgcr-search-pg--next">Next</button>
<span class="wgcr-search-pageof" aria-live="polite">Page 1 of 3</span>
</nav>
| Class | Role |
|---|---|
.wgcr-search-pager | The nav wrapper with an accessible label |
.wgcr-search-pg | Each page button |
.is-active | The current page, with aria-current="page" |
.wgcr-search-pg--prev / --next | Previous and next buttons |
.wgcr-search-pageof | The “Page X of Y” text inside a polite live region |
Related controls
| Control | Id | Where | Values |
|---|---|---|---|
| Pagination type | pagination_type | “Pagination” section | None / Numbers / Previous and next / Load on click / Load on scroll |
| Pagination alignment | pagination_align | “Pagination” section | Right / Centre / Left / Justify, responsive; shown when pagination is not “None” |
| Spacer | pagination_spacer | “Pagination” section | With / Without; adds the wgcr-search--spacer class |
| Items per page | posts_per_page | “Layout” section (Page mode) | 1 to 50, default 12 |
| Button text | button_text | “Pagination” section | Default “View more posts”; load on click only |
| Button icon | button_icon | “Pagination” section | Show / Hide; load on click only |
| Button ID | button_id | “Pagination” section | Printed as the button's id; leave empty to get a generated one |
| Message alignment | no_posts_align | “Pagination” section | Right / Centre / Left / Justify; load on click and load on scroll |
| Custom message | custom_message | “Pagination” section | Off by default; load on click and load on scroll |
| Custom message text | custom_message_text | “Pagination” section | Replaces “No more posts.” when the switcher is on |
The “Spacer” control only adds vertical space before the bar; it does not change behaviour.
How the page count is computed
const totalPages = Number( response.headers.get( 'X-WP-TotalPages' ) ) || 1;
const current = Number( params.get( 'page' ) ) || 1;
The server sends these headers only when page is greater than 0. That means a plain request without page returns no pagination headers, which is why the widget always sends page when a pagination type is active.
Infinite scroll
With load_on_scroll the widget builds a sentinel element at the end of the list:
const io = new IntersectionObserver( ( entries ) => {
if ( entries[ 0 ].isIntersecting && ! loading && hasMore ) {
loadPage( current + 1, true );
}
}, { rootMargin: '300px' } );
| Safeguard | Why |
|---|---|
loading flag | Prevents two requests at once |
hasMore flag | Stops requests after the last page |
rootMargin: 300px | Loads slightly before the visitor reaches the end |
| Disconnect on reset | The observer is removed when the query changes |
Keyboard behaviour
| Key | Result |
|---|---|
Tab | Moves through the page buttons in order |
Enter or Space | Activates the focused page button |
ArrowDown / ArrowUp | Navigate the results list (when focus is on the field or inside the results) |
After a page change, focus returns to the button the visitor pressed — not to the top of the page — so screen-reader users and keyboard users keep their place in the interface.
Performance notes
- Items per page: the default 12 is a good balance. Above 24 results the payload grows noticeably, especially with thumbnails.
- Thumbnails: every result with an image adds a request (unless cached). Turning “Show thumbnail” off makes long lists much lighter.
- Templates: a custom Elementor template renders each result on the server, so more items per page means more work per request. With a heavy template keep “Items per page” at 6 to 9.
- Debounce: queries are debounced at 250 ms and every in-flight request is aborted with
AbortController, so fast typing does not produce a burst of requests.
Choosing a type
| Your case | Best type |
|---|---|
| A search page with many results | Numbers |
| A narrow header panel | Previous and next, or Load on click |
| Browsing a long list without interaction | Load on scroll |
| A small fixed number of results | None |
Styling
In Page mode, extra sections control the container and the cards (background, spacing, border radius, shadow and typography).
The search widget is styled in two layers: Elementor style controls (which print inline CSS on the widget) and CSS classes you can target from your theme. This page covers both, plus ready-made snippets.
CSS variables
The plugin defines component-scoped CSS custom properties on each .wgcr-search root. They are not site-wide theme variables; they provide defaults for the widget’s accent colours, borders, radius and responsive column count. Override them on the root of one widget to change only that instance.
| Variable | Default | Purpose |
|---|---|---|
--wgcr-blue | #2E86DE | Main accent: focus border, active icon and spinner |
--wgcr-navy | #1B5EA8 | Darker shades of the main accent |
--wgcr-ink | #111827 | Heading text colour |
--wgcr-slate | #64748B | Secondary text: excerpt, date and icon |
--wgcr-line | #E2E8F0 | Border colour |
--wgcr-radius | 14px | Field, panel and card radius |
--wgcr-search-cols | 1 | Column count in Page mode; set by the --cols-1 … --cols-6 root classes |
For example, change the palette for one widget:
.wgcr-search {
--wgcr-blue: #4f46e5;
--wgcr-navy: #3730a3;
--wgcr-ink: #0f172a;
--wgcr-slate: #64748b;
--wgcr-line: #e2e8f0;
--wgcr-radius: 10px;
}
The classes below are the stable hooks:
| Class | Applied to | Meaning |
|---|---|---|
.wgcr-search | Root | Every search instance |
.wgcr-search--modal | Root | List mode |
.wgcr-search--page | Root | Page display mode |
.wgcr-search--cols-1 … --cols-6 | Root | Column count |
.wgcr-search--masonry | Root | Masonry layout |
.wgcr-search--equal | Root | Equal-height cards |
.wgcr-search--tpl | Root | A custom Elementor template is in use |
.wgcr-search--spacer | Root | Extra space before pagination |
.wgcr-search-field | The input | The search field |
.wgcr-search-panel | The panel | The results container |
.wgcr-search-list | The list | The results grid or list |
.wgcr-search-item | Each result | One result card |
.wgcr-search-link | Each result | The result link |
.wgcr-search-thumb | Each result | The thumbnail |
.wgcr-search-title | Each result | The title |
.wgcr-search-excerpt | Each result | The excerpt |
.wgcr-search-date | Each result | The date |
.wgcr-search-msg | The panel | “No results” and error messages |
.wgcr-search-pager | The panel | The pagination bar |
.wgcr-search-status | The panel | The aria-live status text |
Elementor style controls
Search field
| Control | Default | Effect |
|---|---|---|
| Background | rgba(255,255,255,0.85) | Field background |
| Background (focus) | — | When the field has focus |
| Text colour | — | The typed query |
| Border colour | — | Field border |
| Border colour (focus) | — | Border on focus |
| Placeholder colour | #94a3b8 | Placeholder text |
| Font size | — | Field text size |
| Border radius | 12px | Field corner radius |
Results box
| Control | Default | Effect |
|---|---|---|
| Background | #ffffff | Panel background |
| Border colour | — | Panel border |
| Border radius | 14px | Panel corner radius |
| Max height | 70vh | Panel height cap in List mode |
| Message text colour | #94a3b8 | “No results” and error messages |
Items
| Control | Default | Effect |
|---|---|---|
| Padding | 12px | Space inside each result |
| Text colour | #0f172a | Result title |
| Background (hover) | #f1f5f9 | The focused or hovered result |
| Separator colour | #f1f5f9 | Line between results |
| Thumbnail width | 72px | Thumbnail size |
| Thumbnail border radius | 10px | Thumbnail corners |
| Excerpt text colour | #64748b | Excerpt text |
| Excerpt font size | — | Excerpt size |
Columns, masonry and equal height
These three settings only appear in Page mode and map directly to root classes:
| Setting | Class | Layout |
|---|---|---|
| Columns 3 | wgcr-search--cols-3 | grid-template-columns: repeat(3, minmax(0,1fr)) |
| Columns 2 | wgcr-search--cols-2 | grid-template-columns: repeat(2, minmax(0,1fr)) |
| Columns 1 | wgcr-search--cols-1 | A single column list |
| Masonry | wgcr-search--masonry | Items keep their natural height |
| Equal height | wgcr-search--equal | All cards stretch to the tallest |
On screens narrower than 782px the grid drops to a single column automatically, and below 480px the thumbnail shrinks so results stay readable on small phones.
Ready-to-use snippets
A card with a soft shadow and a coloured edge
.wgcr-search-item {
border-radius: 12px;
box-shadow: 0 2px 12px rgb(15 23 42 / 8%);
border-inline-start: 3px solid #2563eb;
transition: transform .18s ease, box-shadow .18s ease;
}
.wgcr-search-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgb(15 23 42 / 12%);
}
A dark panel
.wgcr-search-panel {
background: #0f172a;
border-color: #1e293b;
color: #e2e8f0;
}
.wgcr-search-title { color: #f8fafc; }
.wgcr-search-excerpt { color: #94a3b8; }
.wgcr-search-item:hover { background: #1e293b; }
.wgcr-search-msg { color: #64748b; }
A panel as wide as the field in List mode
.wgcr-search--modal .wgcr-search-panel {
position: absolute;
inset-inline: 0;
width: 100%;
max-width: none;
}
Removing the thumbnail for a compact layout
.wgcr-search-thumb { display: none; }
.wgcr-search-item { padding: 8px 12px; }
.wgcr-search-link { gap: 0; }
Important notes
- Do not edit
search.cssdirectly: the file is replaced on every update. Put your CSS in the theme's stylesheet or in Elementor's “Custom CSS”. - Respect
prefers-reduced-motion: if you add transitions, disable them for visitors who asked for reduced motion:
@media (prefers-reduced-motion: reduce) {
.wgcr-search-item, .wgcr-search-panel { transition: none !important; }
}
- Keep contrast: the default colours were chosen for at least 4.5:1 contrast on the body text. If you recolour the panel, re-check contrast (see the accessibility page).
- RTL and LTR: all layout rules use logical properties (
inset-inline,margin-inline,border-inline-start), so the same CSS works in both directions. Avoid physical properties such asleftormargin-rightin your overrides.
Result structure
With the default template each result looks like this:
<article class="wgcr-search-item">
<a class="wgcr-search-link" href="https://example.com/?p=42">
<img class="wgcr-search-thumb" src="…" alt="Post title" width="72" height="72">
<h4 class="wgcr-search-title">Post title</h4>
<p class="wgcr-search-excerpt">A short text…</p>
<time class="wgcr-search-date" datetime="2026-09-01">2026-09-01</time>
</a>
</article>
In custom template mode the markup is whatever your Elementor template produces, and wgcr-search--tpl is added to the root so you can style it separately.
JavaScript configuration
Before search.js, a configuration object is printed inline:
window.WGCRSearch = {
restUrl: 'https://example.com/wp-json/wgcr/v1/search',
homeUrl: 'https://example.com/',
minChars: 2,
maxResults: 50,
i18n: {
noResults: 'No results found.',
found: 'results',
error: 'Error fetching results.',
prev: 'Previous',
next: 'Next',
page: 'Page',
pageOf: 'of'
}
};
| Key | Meaning |
|---|---|
restUrl | The search route address, built with rest_url() |
homeUrl | The site address, used by the real search form |
minChars | Minimum query length before a request is sent (2) |
maxResults | Hard cap on items per page (50) |
i18n | Interface texts, translated into the site language |
Client-side behaviour
| Behaviour | Value |
|---|---|
| Minimum query length | 2 characters (minChars) |
| Delay after typing (debounce) | 250 milliseconds |
| Results cap per request | 50 (WGCR_SEARCH_MAX_RESULTS) |
| Cancelling the previous request | With AbortController while typing fast |
| Endpoint | /wp-json/wgcr/v1/search |
| Infinite scroll | With IntersectionObserver on .wgcr-search-sentinel |
The flow: the visitor types → 250 ms are waited → if the query is shorter than 2 characters the panel is emptied and cleared → otherwise the previous request is aborted and a new one is sent → results are rendered → the status message (“N results”) is read out to screen readers.
Working without JavaScript
The widget's form is a real GET form whose action is the site home, with an s field and a hidden post_type field. So if JavaScript is disabled, submitting the form takes the visitor to WordPress' own search results page for that post type. This is the pattern recommended for accessibility and SEO: progressive enhancement, not a hard dependency on a script.
Data attributes on the widget root
The JavaScript reads its settings from the data-* attributes on the root element, so if you build the widget with dynamic templates or PHP code, include all of them:
| Attribute | Example |
|---|---|
data-wgcr-search | The initialisation marker (no value) |
data-display-mode | modal or page |
data-source | post, page, product, … |
data-limit | 12 |
data-thumb | yes / no |
data-excerpt | yes / no |
data-all | The text of the “show all” link |
data-template | An Elementor template id, or 0 |
data-query | Compact JSON with orderby, order, date, terms, terms_op, ignore_sticky, query_id |
data-pagination | none, numbers, prev_next, load_on_click, load_on_scroll |
Complete control reference
Every content control of the widget, in panel order. Style controls are listed in “Style sections” above.
| Id | Panel label | Type | Default | Shown when |
|---|---|---|---|---|
display_mode | Results display mode | Select | modal (List mode) | — |
placeholder | Placeholder text | Text | Empty (filled automatically) | — |
thumb | Show thumbnail | Switcher | Yes | — |
excerpt | Show excerpt | Switcher | Yes | — |
all_text | “Show all” text | Text | “Show all results” | — |
template_mode | Template choice | Select | default | — |
template | Choose a template | Select with search | Empty | template_mode = custom |
edit_template_link | Edit template | Raw HTML (link) | — | template_mode = custom |
columns | Columns | Responsive number | 3 (tablet 2, mobile 1) | display_mode = page |
posts_per_page | Items per page | Number, 1 to 50 | 12 | — |
masonry | Masonry layout | Switcher | No | display_mode = page |
equal_height | Equal height | Switcher | No | display_mode = page and masonry off |
source | Source | Select | post | — |
custom_type | Custom post type | Text | Empty | source = custom |
include_by | Include by | Select | exclude | — |
include_terms | Categories and tags | Term selector | Empty | — |
date_filter | Date | Select | all | — |
orderby | Order by | Select | relevance | — |
order | Order | Select | DESC | orderby is not relevance or rand |
ignore_sticky | Ignore sticky posts | Switcher | Yes | source = post |
query_id | Query ID | Text | Empty | — |
pagination_type | Pagination | Select | none | — |
pagination_align | Alignment | Responsive select | center | pagination_type is not none |
pagination_spacer | Spacer | Select (with/without) | no | pagination_type is not none |
button_heading | Button | Separator heading | — | pagination_type = load_on_click |
button_text | Button text | Text | “View more posts” | pagination_type = load_on_click |
button_icon | Button icon | Select (show/hide) | yes | pagination_type = load_on_click |
button_id | Button ID | Text | Empty | pagination_type = load_on_click |
no_posts_heading | “No more posts” message | Separator heading | — | load_on_click or load_on_scroll |
no_posts_align | Message alignment | Responsive select | center | load_on_click or load_on_scroll |
custom_message | Custom message | Switcher | No | load_on_click or load_on_scroll |
custom_message_text | Custom message text | Textarea | Empty | The above plus custom_message = yes |
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Nothing appears while typing | The query is shorter than 2 characters | Type two characters or more |
| “Error fetching results.” | The REST API is blocked, or a server error | Run curl against /wp-json/wgcr/v1/search?q=test |
| Duplicate results | Two widgets sharing the same button id | Use a unique id, or leave it empty |
| Result images are empty | The post has no featured image | The .wgcr-search-thumb--empty class fills the space; or turn “Show thumbnail” off |
| Columns have no effect | The display mode is List mode | Switch the mode to Page mode |
| Works in the editor but not on the site | Page cache or a JS optimiser | Exclude search.js from combining or delaying |