Creating a multi-column layout
Improving the search experience often means adding several sources of data, such as in a federated search. Doing this in an organized and efficient way can be challenging. To make the most od larger display on devices such as desktops and tablets, you should display data in a multi-column layout. This lets users quickly browse search results and reduces scrolling.
Before you begin
This tutorial assumes the following requirements:
- An installation of the
autocomplete-js
package. - A populated Query Suggestions index. If you don’t have one, create it first: you can use the example application credentials and index provided in this tutorial.
- Existing HTML with an input element where you want to insert the autocomplete drop-down menu.
- Frontend development experience with HTML, CSS, and JavaScript.
Starter code
This tutorial builds an Autocomplete app composed of two columns:
- A left column taking 1/3 of the width which displays recent searches and Query Suggestions
- A right column taking up the rest of the space which displays products.
Create a file called app.js
in your src
directory and add this code to it:
1
2
3
4
5
6
7
8
import { autocomplete } from '@algolia/autocomplete-js';
import '@algolia/autocomplete-theme-classic';
autocomplete({
container: '#autocomplete',
openOnFocus: true,
});
This starter code assumes you want to insert the autocomplete into a DOM element with autocomplete
as an id
.
Change the container
to match your markup.
Setting openOnFocus
to true
ensures that the drop-down menu appears as soon as a user focuses the input.
Two-column layout
With the render
function of autocomplete-js
, you can customize the panel rendering to create two or more columns.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { autocomplete } from '@algolia/autocomplete-js';
import '@algolia/autocomplete-theme-classic';
autocomplete({
container: '#autocomplete',
openOnFocus: true,
plugins: [],
render({ render, html }, root) {
render(
html`<div class="aa-PanelLayout aa-Panel--scrollable">
<div class="aa-PanelSections">
<div class="aa-PanelSection--left"></div>
<div class="aa-PanelSection--right"></div>
</div>
</div>`,
root
);
},
});
Add recent searches and Query Suggestions
Add the recent searches (with a limit of two recent searches) and Query Suggestions plugin (limiting the number of suggestions to seven) to the left column. This gives a total of nine items in the right column.
Then, retrieve the recent searches and query suggestions plugins sources in the render
function using elements
and add it to the left column.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// ...
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import '@algolia/autocomplete-theme-classic';
// You can use the example application credentials in case you don't have your own yet.
// const appId = 'latency';
// const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: 'multi-column-layout-example',
limit: 2,
});
const querySuggestionsPlugin = createQuerySuggestionsPlugin({
searchClient,
indexName: 'autocomplete_demo_products_query_suggestions',
getSearchParams() {
return {
...recentSearchesPlugin.data.getAlgoliaSearchParams(),
hitsPerPage: 7,
};
},
});
autocomplete({
container: '#autocomplete',
openOnFocus: true,
plugins: [recentSearchesPlugin, querySuggestionsPlugin],
render({ elements, render, html }, root) {
const { recentSearchesPlugin, querySuggestionsPlugin } = elements;
render(
html`<div class="aa-PanelLayout aa-Panel--scrollable">
<div class="aa-PanelSections">
<div class="aa-PanelSection--left">
${recentSearchesPlugin} ${querySuggestionsPlugin}
</div>
<div class="aa-PanelSection--right"></div>
</div>
</div>`,
root
);
},
});
Create a “products” source
Add a source to retrieve and display the products in the right column.
Create a dynamic source using the getSources
function and retrieve the products from your Algolia index with the getAlgoliaResults
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// ...
autocomplete({
container: '#autocomplete',
openOnFocus: true,
plugins: [recentSearchesPlugin, querySuggestionsPlugin],
getSources({ query }) {
return [
// ...
{
sourceId: 'products',
getItems() {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'autocomplete_demo_products',
params: {
query,
hitsPerPage: 4,
},
},
],
});
},
// ...
},
];
},
render({ elements, render, html }, root) {
const { recentSearchesPlugin, querySuggestionsPlugin } = elements;
render(
html`<div class="aa-PanelLayout aa-Panel--scrollable">
<div class="aa-PanelSections">
<div class="aa-PanelSection--left">
${recentSearchesPlugin} ${querySuggestionsPlugin}
</div>
<div class="aa-PanelSection--right"></div>
</div>
</div>`,
root
);
},
});
Display products
Now that you’ve set up your source, you can display products with templates.
Create a product item component
Start by creating a file called ProductItem.js
in your src
directory, then copy and paste the following code (a component used to render each item of your source):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
function cx(...classNames) {
return classNames.filter(Boolean).join(' ');
}
function formatPrice(value, currency) {
return value.toLocaleString('en-US', { style: 'currency', currency });
}
export function ProductItem({ html, hit, components }) {
return html`
<a
href="https://example.org/"
target="_blank"
rel="noreferrer noopener"
class="${cx('aa-ItemLink', hit.objectID)}"
>
<div class="aa-ItemContent">
<div class="aa-ItemPicture">
<img
src="${hit.image_urls[0]}"
alt="${hit.name}"
onLoad=${() => {
const imgEl = document.querySelector(
`.${hit.objectID} .aa-ItemPicture`
);
imgEl.classList.add('aa-ItemPicture--loaded');
}}
/>
</div>
<div class="aa-ItemContentBody">
<div>
${hit.brand &&
html`
<div class="aa-ItemContentBrand">
${components.Highlight({ hit, attribute: 'brand' })}
</div>
`}
<div class="aa-ItemContentTitleWrapper">
<div class="aa-ItemContentTitle">
${components.Highlight({ hit, attribute: 'name' })}
</div>
</div>
</div>
<div>
<div class="aa-ItemContentPrice">
<div class="aa-ItemContentPriceCurrent">
${formatPrice(hit.price.value, hit.price.currency)}
</div>
${hit.price.on_sales &&
html`
<div class="aa-ItemContentPriceDiscounted">
${formatPrice(hit.price.discounted_value, hit.price.currency)}
</div>
`}
</div>
<div class="aa-ItemContentRating">
<ul>
${Array(5)
.fill(null)
.map(
(_, index) =>
html`<li key="${index}">
<div
class="${cx(
'aa-ItemIcon aa-ItemIcon--noBorder aa-StarIcon',
index >= hit.reviews.rating && 'aa-StarIcon--muted'
)}"
>
<svg viewBox="0 0 24 24" fill="currentColor">
<path
d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"
/>
</svg>
</div>
</li>`
)}
</ul>
<span class="aa-ItemContentRatingReviews">
(${hit.reviews.count})
</span>
</div>
</div>
</div>
</div>
</a>
`;
}
Render each source item
To render each source item, use the templates
option in your source with the item
function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ...
autocomplete({
// ...
getSources() {
return [
// ...
{
sourceId: 'products',
getItems({ /* ... */ }) {
// ...
},
templates: {
item({ html, item, components }) {
return ProductItem({ html, hit: item, components });
},
},
// ...
},
];
},
render({ elements, render, html }, root) {
const { recentSearchesPlugin, querySuggestionsPlugin, products } = elements;
render(
html`<div class="aa-PanelLayout aa-Panel--scrollable">
<div class="aa-PanelSections">
<div class="aa-PanelSection aa-PanelSection--left">
${recentSearchesPlugin} ${querySuggestionsPlugin}
</div>
<div class="aa-PanelSection aa-PanelSection--right">
${products}
</div>
</div>
</div>`,
root
);
},
});
Add styles
Copy and paste the following CSS snippet in your src
directory and call it style.css
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
* {
box-sizing: border-box;
}
body {
background-color: rgb(244, 244, 249);
color: rgb(65, 65, 65);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 1rem;
}
.container {
margin: 0 auto;
max-width: 1024px;
width: 100%;
}
/* Panel section */
.aa-PanelSections {
column-gap: var(--aa-spacing);
display: flex;
}
.aa-PanelSection {
display: flex;
flex-direction: column;
}
.aa-PanelSection--left {
width: 30%;
}
.aa-PanelSection--right {
width: 70%;
}
/* Item */
.aa-ItemPicture {
width: 100%;
height: 100%;
border-radius: 3px;
overflow: hidden;
background: rgba(var(--aa-muted-color-rgb), 0.2);
}
.aa-ItemPicture img {
object-fit: cover;
width: 100%;
height: auto;
opacity: 0;
transition: opacity 0.2s ease-out;
}
.aa-ItemPicture--loaded img {
opacity: 1;
}
/* Products */
/* --- Common */
.aa-Source[data-autocomplete-source-id='products'] .aa-List {
display: grid;
grid-template-columns: repeat(4, 1fr);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-Item {
padding: var(--aa-spacing-half);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemLink {
justify-content: stretch;
height: 100%;
}
/* --- Content */
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContent {
display: flex;
flex-direction: column;
height: 100%;
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContent mark {
color: rgb(var(--aa-primary-color-rgb));
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentBody {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: var(--aa-spacing-half);
}
/* --- Brand */
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentBrand {
font-size: 0.7em;
text-transform: uppercase;
color: rgb(var(--aa-muted-color-rgb));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentBrand mark {
font-weight: normal;
}
/* --- Title */
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentTitleWrapper {
height: calc(var(--aa-spacing) * 2.5);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentTitle {
font-size: 0.9em;
margin: 0;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
white-space: normal;
}
/* --- Price */
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentPrice {
display: flex;
column-gap: var(--aa-spacing-half);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentPriceCurrent {
font-weight: bold;
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentPriceDiscounted {
font-size: 0.9em;
text-decoration: line-through;
color: rgb(var(--aa-muted-color-rgb));
}
/* --- Rating */
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentRating ul {
display: flex;
list-style: none;
padding: 0;
}
.aa-Source[data-autocomplete-source-id='products'] .aa-ItemContentRating {
display: flex;
align-items: center;
column-gap: calc(var(--aa-spacing-half) / 2);
margin-top: var(--aa-spacing-half);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-StarIcon {
width: 1.3em;
height: 1.3em;
color: #fdbc72;
}
.aa-Source[data-autocomplete-source-id='products'] .aa-StarIcon--muted {
color: #d6d6e6;
}
.aa-Source[data-autocomplete-source-id='products']
.aa-ItemContentRatingReviews {
font-size: 0.7em;
color: #908eae;
}
/* Media queries */
@media screen and (max-width: 680px) {
/* Panel section */
.aa-PanelSections {
flex-direction: column;
row-gap: var(--aa-spacing);
}
.aa-PanelSection--left,
.aa-PanelSection--right {
width: 100%;
}
/* Products */
.aa-Source[data-autocomplete-source-id='products'] .aa-List {
display: flex;
flex-wrap: wrap;
gap: var(--aa-spacing-half);
}
.aa-Source[data-autocomplete-source-id='products'] .aa-Item {
width: calc(50% - var(--aa-spacing-half) / 2);
}
}
Make sure to include it in your project’s app.js
file.
1
2
3
4
5
6
7
// ...
import './style.css';
autocomplete({
// ...
});
Next steps
This tutorial focuses on building a multi-column layout but you can improve it by:
- Adding more sources
- Reshaping and combine your sources
- Adding no-result states
For example, this two-column layout example implements these features.