UI Libraries / Autocomplete / Creating a multi-column layout

Creating a multi-column layout

Improving the search experience often means adding multiple sources of data, such as in a federated search. However, displaying multiple data sources in an organized and efficient way can be challenging. To leverage the larger display on devices such as desktops and tablets, it’s recommended to display data in a multi-column layout. This allows users to navigate more quickly through the search results and reduces scrolling.

An experience using a two-column layout with Autocomplete

Getting started

You are going to build an Autocomplete demo composed of two columns:

First, begin with some boilerplate for the autocomplete implementation.
Create a file called app.js in your src directory, and add the code below:

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 boilerplate assumes you want to insert the autocomplete into a DOM element with autocomplete as an id. You should change the container to match your markup. Setting openOnFocus to true ensures that the dropdown appears as soon as a user focuses the input.

Creating a two-column layout

Using the render function of autocomplete-js, you can customize the panel rendering to create two columns or more.

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 className="aa-PanelLayout aa-Panel--scrollable">
        <div className="aa-PanelSections">
          <div className="aa-PanelSection--left"></div>
          <div className="aa-PanelSection--right"></div>
        </div>
      </div>`,
      root
    );
  },
});

Adding recent searches and query suggestions

To continue, add the Recent Searches plugin and the Query Suggestions plugin to the left column.

First, you need to create an Algolia search client and create the recent searches plugin with a limit of two recent searches. After, you can create the query suggestions plugin limiting the number of suggestions to seven.

This gives you a total of nine items in the right column (combining recent searches and query suggestions).

Then, you can retrieve the recent searches and query suggestions plugins sources in the render function using elements and adding 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
// ...
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches';
import { createQuerySuggestionsPlugin } from '@algolia/autocomplete-plugin-query-suggestions';
import algoliasearch from 'algoliasearch/lite';

import '@algolia/autocomplete-theme-classic';

const appId = 'latency';
const apiKey = '6be0576ff61c053d5f9a3225e2a90f76';
const searchClient = algoliasearch(appId, apiKey);

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 className="aa-PanelLayout aa-Panel--scrollable">
        <div className="aa-PanelSections">
          <div className="aa-PanelSection--left">
            ${recentSearchesPlugin} ${querySuggestionsPlugin}
          </div>
          <div className="aa-PanelSection--right"></div>
        </div>
      </div>`,
      root
    );
  },
});

Creating a “products” source

Now you can add a source to retrieve and display the products in the right column.

To do that, you can create a dynamic source using the getSources function and retrieve the products from your Algolia index using 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',
                query,
                params: {
                  hitsPerPage: 4,
                },
              },
            ],
          });
        },
        // ...
      },
    ];
  },
  render({ elements, render, html }, root) {
    const { recentSearchesPlugin, querySuggestionsPlugin } = elements;

    render(
      html`<div className="aa-PanelLayout aa-Panel--scrollable">
        <div className="aa-PanelSections">
          <div className="aa-PanelSection--left">
            ${recentSearchesPlugin} ${querySuggestionsPlugin}
          </div>
          <div className="aa-PanelSection--right"></div>
        </div>
      </div>`,
      root
    );
  },
});

Displaying products

Now that you set up your source, you can display products using the Templates API.

Creating a product item component

Start by creating a file called ProductItem.js in your src directory and copy/paste the snippet below, it’s 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>
  `;
}

Rendering each source item

To render each source item, you can 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 className="aa-PanelLayout aa-Panel--scrollable">
        <div className="aa-PanelSections">
          <div className="aa-PanelSection aa-PanelSection--left">
            ${recentSearchesPlugin} ${querySuggestionsPlugin}
          </div>
          <div className="aa-PanelSection aa-PanelSection--right">
            ${products}
          </div>
        </div>
      </div>`,
      root
    );
  },
});

Adding styles

You can copy/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 guide focuses on building a multi-column layout, but you can improve this demo by trying to:

You can take a look at the two-column layout example that implements the previous features.

Did you find this page helpful?