UI customizations
Out of the box, an Alokai application ships with a default design system built on top of Storefront UI. While this helps ensure that your application looks good instantly, it's likely one of the first things you'll want to customize.
Since you have control over the storefront's code in either the Next.js or Nuxt application, you have access to all the code that can modify the look and feel of the application. This guide will walk you through some of the most common customization scenarios and will hopefully help you get familiar with the structure of the codebase.
In this chapter, you will:
- change the default logo to your custom one
- adjust the theme colors to suit the new logo
- add a pre-header with i18n
- customize the look of product cards on the product listing page
- add facet/filter search feature
In the end, your application will look something like this:
Changing the logo
The first step to making any changes is to identify what component is actually responsible for certain elements.
In this example, we need to find and modify the component that contains the logo. You can either:
- Drill through the Next.js application starting from
apps/storefront-unified-nextjs/app/[locale]/(cms)/[[...slug]]/page.tsx
, which represents the homepage (actually all CMS pages). The layout for this page is located inapps/storefront-unified-nextjs/app/[locale]/(cms)/layout.tsx
, which re-exportsBaseDefaultLayout
. In that layout you can find thatNavbar
component is responsible for rendering the navbar. Within that component we haveNavbarTop
component which renders the logo. - Or you can use React Developer Tools to localize the component visually:
Doing one of these, you'll find that the logo is located in apps/storefront-unified-nextjs/components/navigations/navbar-top.tsx
component.
Now, we have to change that component and replace SfIconAlokaiFull
with your custom image. For this example, let's use LogoIpsum to generate a sample logo. Download a sample logo and place it in apps/storefront-unified-nextjs/public/images
folder.
<SfIconAlokaiFull data-testid="logo" className="h-full w-auto" />
<Image src="/images/<logo-file-name>" width={100} height={50} unoptimized alt="logo" />
The result should look like this:
However, this logo doesn't look good on the storefront's default green background. We can change that by removing the filled
property from NavbarTop
component in Navbar
component. This will make the navbar transparent.
- <NavbarTop filled>
+ <NavbarTop>
The logo should look better now. However, buttons in the navbar have become invisible. That's because they are white.
An Extra Challenge
Within the NavbarTop
component, find which tailwind class is responsible for making the buttons white and remove it.
Adjusting theme colors
The primary green color does not play well with the colors in the logo. To fix this we will adjust our theme colors.
The Storefront uses custom Tailwind CSS colors throughout the application, so you can make large UI changes by adjusting the colors in the tailwind.config.ts
file. If you don't have a color palette already, you can use Tailwind Colors to generate one.
Then, edit apps/storefront-unified-nextjs/tailwind.config.ts
and paste your colors under theme.extend
, rename the color to "primary"
// ...
theme: {
extend: {
colors: {
primary: {
50: '#fff0f9',
100: '#ffe3f5',
200: '#ffc6eb',
300: '#ff98d9',
400: '#ff58bd',
500: '#ff27a1',
600: '#ff0c81',
700: '#df005f',
800: '#b8004f',
900: '#980345',
950: '#5f0025',
},
},
// ...
You can read more about theming in the Storefront UI docs.
Adding a pre-header
A common use case is to add a pre-header to the top of each page with something like promotional codes or a call to action. If you feel confident that you can do this, try it out before reading on.
Solution
- Create a new
pre-header.tsx
file inapps/storefront-unified-nextjs/components/navigations
folder with the following content:
import { SfIconInfo } from '@storefront-ui/react';
export function PreHeader() {
return (
<div
role="alert"
className="flex items-center w-full justify-center shadow-md bg-secondary-100 pr-2 pl-4 ring-1 ring-secondary-200 typography-text-sm md:typography-text-base py-1 rounded-md"
>
<SfIconInfo className="mr-2 text-secondary-700 shrink-0" />
Limited offer. Use code: ALOKAI2024
</div>
);
}
- Add
PreHeader
component toNavbarTop
:
export default function NavbarTop({ children, className, filled }: NavbarTopProps) {
const t = useTranslations('NavbarTop');
const messages = useMessages();
return (
<> <PreHeader /> <header
className={classNames(
'sticky top-0 z-40 flex h-14 md:-top-5 md:h-20 md:pt-2.5',
filled ? 'bg-primary-700 md:shadow-md' : 'border-b border-neutral-200 bg-white text-neutral-900',
className,
)}
data-testid="navbar-top"
>
<div className="sticky top-0 mx-auto flex w-full max-w-screen-3-extra-large items-center gap-[clamp(1rem,3vw,3rem)] px-4 py-6 md:h-[60px] md:px-6 lg:px-10">
<Link className="-mt-1.5 h-6 md:h-7" data-testid="logo-link" href="/" title={t('homepage')}>
<Image src="/images/logoipsum-332.svg" width={100} height={50} unoptimized alt="logo" />
</Link>
{children}
<NextIntlClientProvider messages={pick(messages, 'Notifications')}>
<Notifications />
</NextIntlClientProvider>
</div>
</header>
</> );
}
Pre-header internationalization (i18n)
Let's make this example more interesting by making the pre-header localized. next-intl package comes installed in your storefront and is our recommended solution for internationalization.
- First, we need to add translations. Translation files are located under
apps/storefront-unified-nextjs/lang
folder. There's a separate subfolder for each language (e.g.en
,de
). Open bothen/base.json
andde/base.json
files and add a new translation there:
},
+ "PreHeader": {
+ "promoText": "Limited offer. Use code: ALOKAI2024"
+ },
"Navbar": {
},
+ "PreHeader": {
+ "promoText": "Begrenztes Angebot. Verwenden Sie den Code: ALOKAI2024"
},
"Navbar": {
- Now, we can use the translations in our
PreHeader
component. We'll utilize theuseTranslations
fromnext-intl
package. Yourpre-header.tsx
should look like this now:
import { SfIconInfo } from '@storefront-ui/react';
import { useTranslations } from 'next-intl';
export function PreHeader() {
const t = useTranslations('PreHeader');
return (
<div
role="alert"
className="flex w-full items-center justify-center rounded-md bg-secondary-100 py-1 pl-4 pr-2 shadow-md ring-1 ring-secondary-200 typography-text-sm md:typography-text-base"
>
<SfIconInfo className="mr-2 shrink-0 text-secondary-700" />
{t('promoText')}
</div>
);
}
Modifying the product card on PLP
As a challenge, try to implement these design changes on your own by making the changes to the ProductCardVertical
component.
To check your solution, you can look at our implementation.
Facet search on PLP
Now let's try something more ambitious - we'll extend facet's (aka filters) behavior. We want to be able to filter/search through the facets.
Before jumping to the solution, think about how would you do this yourself. Investigate the application and which parts you need to modify.
Solution
- Under
apps/storefront-unified-nextjs/components/products-listing
create a newFilterSearch
component that would be our searchbox. As a starting point, you can use Storefront UI's Search Block.apps/storefront-unified-nextjs/components/products-listing/filter-search.tsximport { SfIconCancel, SfIconSearch, SfInput } from '@storefront-ui/react'; import { useRef, useState, type ChangeEvent, type FormEvent, type KeyboardEvent } from 'react'; export type FilterSearchProps = { onSearch: (value: string) => void; }; export default function FilterSearch({ onSearch }: FilterSearchProps) { const inputRef = useRef<HTMLInputElement>(null); const [searchValue, setSearchValue] = useState(''); const isResetButton = Boolean(searchValue); const handleSubmit = (event: FormEvent) => { event.preventDefault(); }; const handleFocusInput = () => { inputRef.current?.focus(); }; const handleReset = () => { setSearchValue(''); onSearch(''); handleFocusInput(); }; const handleChange = (event: ChangeEvent<HTMLInputElement>) => { const ph = event.target.value; setSearchValue(ph); onSearch(ph); }; const handleInputKeyDown = (event: KeyboardEvent<HTMLDivElement>) => { if (event.key === 'Escape') handleReset(); }; return ( <form role="search" onSubmit={handleSubmit} className="relative px-4 py-1"> <SfInput ref={inputRef} value={searchValue} onChange={handleChange} aria-label="Search" placeholder="Search 'MacBook' or 'iPhone'..." onKeyDown={handleInputKeyDown} slotPrefix={<SfIconSearch />} slotSuffix={ isResetButton && ( <button type="reset" onClick={handleReset} aria-label="Reset search" className="flex rounded-md focus-visible:outline focus-visible:outline-offset" > <SfIconCancel /> </button> ) } /> </form> ); }
- Modify
Facet
component inapps/storefront-unified-nextjs/components/products-listing/facets.tsx
by addingFilterSearch
component and implementing filtering logic:apps/storefront-unified-nextjs/components/products-listing/facets.tsxfunction Facet({ containerClassName, expandableListProps, facet, itemRenderer: FacetItem, multiSelect = false, }: FacetProps) { const [searchPhrase, setSearchPhrase] = useState(''); const [values, setValues] = useState(facet.values); useEffect(() => { if (searchPhrase === '') { setValues(facet.values); } else { setValues( facet.values.filter((item) => item.label.toLocaleLowerCase().includes(searchPhrase.toLocaleLowerCase())), ); } }, [facet, searchPhrase]); const [selected, setSelected] = useQueryState( `${FACET_QUERY_PREFIX}${facet.name}`, parseAsArrayOf(parseAsString).withDefault([]).withOptions({ shallow: false }), ); function toggleFacet(value: string) { if (selected.includes(value)) { const updated = selected.filter((v) => v !== value); return setSelected(updated.length ? updated : null); } return setSelected(multiSelect ? [...selected, value] : [value]); } return ( <AccordionItem className="border-b border-neutral-200 pb-6" id={facet.name} key={facet.name} summary={ <span className="text-base font-medium capitalize" data-testid={`filter-${facet.label.replace(/\s+/g, '-').toLowerCase()}-heading`} > {facet.label} </span> } summaryClassName="pt-4" > <FilterSearch onSearch={setSearchPhrase}></FilterSearch> <div className={containerClassName}> <ExpandableList {...expandableListProps}> {facet.values.map((item) => ( {values.map((item) => ( <FacetItem key={item.value} {...item} onItemClick={() => toggleFacet(item.value)} selected={selected.includes(item.value)} /> ))} </ExpandableList> </div> </AccordionItem> ); }
You can find a complete project example in this repository: https://github.com/vsf-customer/extensibility-demo-v2. If you want to get access to it, contact our sales team.