How to build own NFT marketplace with HTML&CSS like Shopify

Ropital
NFT Application Development
13 min readJun 14, 2023

In this article, we will introduce how to easily build an NFT store or NFT marketplace for selling your own NFTs using HTML and CSS.

When selling NFTs, brand strategy is a crucial element.

To that end, it is necessary to create your own NFT marketplace, landing pages, and gallery pages to enhance your brand.

In this tutorial, we will explain how to build an NFT store like the one below.

https://panda-sticker-punks.mycocoshop.io

If you have any questions or want to share information with the community, please feel free to join our Discord.

Step 1: Setup

Mint NFTs

First, let’s create some test NFTs. Log in to CocoShop and create your NFTs.

https://cocoshop.io/login

  1. Click ‘Collections’ from the sidebar → Click ‘Create collection’ → Fill in the details and click ‘Create’
  2. Click ‘Products’ from the sidebar → Click ‘Create item’ → Fill in the details and click ‘Create’
  3. Select the NFT you created → Click ‘Sell’ → Fill in the details and click ‘Sell’

Create an API key

Next, go to the online store page and create an API key for theme development.

First, click ‘Open online store’.

Then, click ‘API keys’ from the sidebar, and within the page click ‘Create an api key’.

Make sure to save the API key that is displayed. You will need this key in the next step, so be sure to save it in a safe place.

If you forget to save it, you can follow the same steps to create a new API key.

Install CocoShop CLI

Next, you will need to install CocoShop CLI. Use the following commands to install CocoShop CLI:

For npm:

$ npm install -g @cocoshop/cli

For yarn:

$ yarn global add @cocoshop/cli

Initialize Project with CocoShop CLI

As the final step in the setup, we will initialize the project with CocoShop CLI.

$ cocoshop theme init

Upon executing the above command, you will be prompted to enter the API key and Secret key. Please enter the API key and Secret key you created in “Create an API key”.

Test a theme

Now that the setup is complete, let’s try uploading the theme using the following command as a test.

$ cocoshop theme upload

Once the upload is complete, a link to the theme editor will be displayed in the command line. Click it to make sure it is displayed correctly.

The setup is now complete. Please proceed to the next step.

Basics

First, let’s briefly explain the syntax used in this article.

CocoShop uses Liquid, a language that wraps HTML and CSS, just like Shopify. It is essentially the same as HTML and CSS, but there are concepts like tags and objects in the context of Liquid.

It’s easy to learn, so there’s no problem.

Tags

Tags create the logic and control flow of the template. They are written using {% %} as shown below.

{% if product.available %}
In stock
{% else %}
Out of stock
{% endif %}

In addition to the if tag, there are other tags like for and section.

For more details, please refer to the documentation.

Objects

Objects contain the content to be displayed on the page. Objects are displayed when enclosed in {{ }}.

{% if connected_wallet %}
Hello {{ connected_wallet.address }}!
{% endif %}

For a list of available objects, please see the documentation.

Filters

Filters modify the output of Liquid objects or variables. They are used within double curly braces {{ }} and variable assignments, and are separated by a pipe character |.

{% if connected_wallet %}
Hello {{ connected_wallet.address | omit_address }}!
{% endif %}

Step 2: Create a Header

First, let’s build the appearance of the Header.

As the Header is used on various pages, create it in the snippets directory. In the snippets directory, store files like header, item-card, modal which are used repeatedly.

/snippets/header.liquid :

<header class="header">
<div class="header_left">
<a class="header_left-logo_text" href="{{ linklists.mainMenu.link.home.url }}">
<img class="header_logo" src="{{ settings.header_logo }}">
</a>

<nav class="header_left-nav">
{%- for link in linklists.mainMenu.links -%}
<a href="{{ link.url }}" class="header_left-nav_item">{{ link.title }}</a>
{%- endfor -%}
</nav>
</div>
<div class="header-user_info">
{%- if connected_wallet -%}
<div class="chain_name">
{{ connected_wallet.chain.name }}
</div>
<div class="header-avatar_wrapper">
<img
src="{{ connected_wallet.icon_image_url }}"
alt="Avatar"
class="header-user_info_avatar">
<span class="header-user_info_username">{{ connected_wallet.address | omit_address }}</span>
</div>
{%- else -%}
<button class="blue_btn">Connect wallet</button>
{%- endif -%}
</div>
</header>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 48px;
}
.header_left-logo_text {
color: #fff;
text-decoration: none;
}
.header_left {
display: flex;
align-items: center;
gap: 32px;
color: white;
}
.header_logo {
width: 160px;
height: auto;
}
.header_left-nav {
opacity: 0.6;
display: flex;
gap: 32px;
}
.header_left-nav_item {
color: #fff;
text-decoration: none;
}
.header_left-nav_item:hover {
color: #ccc;
}
.header-user_info {
display: flex;
align-items: center;
position: relative;
}
.chain_name {
color: #fff;
margin-right: 10px;
border-radius: 10px;
border: 1px solid #fff;
padding: 8px 16px;
margin-right: 8px;
}
.header-user_info_avatar {
width: 40px;
height: 40px;
border-radius: 50%;
margin-right: 10px;
}
.header-user_info_username {
color: #fff;
margin-right: 10px;
}
.header-avatar_wrapper {
display: flex;
align-items: center;
}
</style>

To test it once, let’s display the header in /templates/index.liquid.

{% render 'header' %}

<style>
body {
padding: 0;
margin: 0;
background-color: #15161a;
color: white;
}
</style>

Now, apply the current theme and check the theme editor through the link.

$ cocoshop theme upload

From the sidebar of the theme editor, open the global settings, click on Header, change the logo, and verify that it is reflected in the preview on the right.

Step 3: Implement Wallet Connection Functionality in the Header

Next, we will implement actions such as wallet connection in the header created above.

First, let’s create a wallet modal.

The assets directory can be used for CSS, JavaScript, image files, and more.

The connectWallet function used in JavaScript is defined by CocoShop. There are also other convenient functions defined for communication with MetaMask and contracts.

Please refer to the documentation for a list of functions.

/assets/index.css :

/* ========== Modal ========== */

.modal {
display: none;
position: fixed;
z-index: 9999;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal_content {
background-color: rgb(13, 17, 28);
margin: 15% auto;
padding: 32px;
padding-top: 32px;
width: 500px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
position: relative;
}
.modal_body {
margin-bottom: 16px;
}
.modal_close {
color: #aaaaaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
position: absolute;
top: 24px;
right: 24px;
line-height: 0.7;
}
.modal_title {
font-size: 1.5rem;
margin-bottom: 16px;
}
.modal_close:hover,
.modal_close:focus {
color: white;
text-decoration: none;
cursor: pointer;
}

/snippets/connect_modal.liquid:

<div class="modal" id="connect_modal">
<div class="modal_content">
<span class="modal_close" id="closeConnectModal">×</span>
<h2 class="modal_title">Connect a wallet</h2>
<p class="modal_body">By connecting a wallet, you agree Terms of Service and consent to its Privacy Policy.</p>

<button class="metamask_btn" id="connect_btn">
<img src="{{ 'metamask-icon.svg' | asset_url }}" class="button_icon">
Metamask
</button>
</div>
</div>

<style>
.metamask_btn {
border: none;
background: rgb(19, 26, 42);
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 18px;
margin: 4px 2px;
width: 100%;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 16px;
}
.metamask_btn:disabled {
background-color: gray;
}
.metamask_btn:disabled:hover {
background-color: gray;
}
.metamask_btn:hover {
background-color: rgba(173, 188, 255, 0.24);
}
.button_icon {
margin-right: 8px;
display: inline-block;
height: 20px;
width: 20px;
}
.btn {
background-color: #4caf50;
border: none;
color: white;
padding: 12px 24px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 4px 2px;
cursor: pointer;
border-radius: 8px;
transition: background-color 0.3s;
}
.btn:hover {
background-color: #45a049;
}
</style>

/assets/index.js

// ========== WALLET MODAL ========== //
function openWalletModal(action) {
const connectWalletModal = document.getElementById('connect_modal');
connectWalletModal.style.display = 'block';
window.addEventListener('click', (event) => {
if (event.target === connectWalletModal) {
connectWalletModal.style.display = 'none';
window.removeEventListener('click', () => {});
}
});
const connectWalletButton = document.getElementById('connect_btn');
connectWalletButton.addEventListener('click', () => {
connect(action);
});
}
function closeWalletModal() {
const connectWalletModal = document.getElementById('connect_modal');
connectWalletModal.style.display = 'none';
}
function connect(action) {
const connectButton = document.getElementById('connect_btn');
connectButton.disabled = true;
connectButton.innerHTML = 'Connecting...';
if (action) {
localStorage.setItem('previousAction', action);
}
connectWallet()
.then(() => {
connectModal.style.display = 'none';
connectButton.disabled = false;
connectButton.innerHTML = 'Metamask';
})
.catch((error) => {
console.log(error);
connectButton.disabled = false;
connectButton.innerHTML = 'Metamask';
});
}

Load index.css and index.js in theme.liquid.

/layouts/theme.liquid :

<!doctype html>
<html>
<head>
<link rel="stylesheet" href="{{ 'index.css' | asset_url }}">
<script src="{{ 'index.js' | asset_url }}"></script>
</head>
<body>
...
</body>
</html>

Finally, update header.liquid.

<header class="header">
<div class="header_left">
...
</div>

<div class="header-uszer_info">
{%- if connected_wallet -%}
...
<div class="header-avatar_wrapper">
...

追加↓
{% render 'account-menu' %}
</div>

{%- else -%}
onclick="openWalletModal()"を追加↓
<button class="blue_btn" onclick="openWalletModal()">Connect wallet</button>
{%- endif -%}
</div>
</header>
{% render 'wallet-modal' %}

Run cocoshop theme upload again and check.

You can check the execution of actions like this in the preview, not in the theme editor. Please click the “Preview” link in the header of the theme editor.

From the preview screen displayed in a separate tab, try executing Connect Wallet.

Step 4: Create a Landing Page

Next, update the Index page which will serve as the Landing Page.

Please refer to this document for more details about the Index page.

First, let’s implement the top-section below.

Create top-section.liquid in the sections directory.

We create it in the sections directory to make section-specific settings. The header logo we set earlier is common to all pages, so it was described in settings_schema.json, but the top-section is used only on the index page, so we use a section and describe the settings' JSON inside the section file.

For more details, please see these documents:

Now let’s create top-section.liquid.

<div class="top_section-container">
<div class="top_section_box_wrapper">
<div class="top_section_box">
<div class="top_section_bg_image"></div>
<div class="top_section_box_content">
<h1 class="top_section_title">{{ section.settings.title }}</h1>
<p class="top_section_description">
{{ section.settings.description }}
</p>

<a href="#about">
<button class="blue_btn" id="open_connect_modal">Get started</button>
</a>
</div>
</div>
</div>
<div class="top_section-slider">
<div class="top_section_slider-slide_track">
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_1 }}" alt="Image 1">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_2 }}" alt="Image 2">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_3 }}" alt="Image 3">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_4 }}" alt="Image 4">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_5 }}" alt="Image 5">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_6 }}" alt="Image 6">
</div>
</div>
<div class="top_section_slider-slide_track">
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_1 }}" alt="Image 1">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_2 }}" alt="Image 2">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_3 }}" alt="Image 3">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_4 }}" alt="Image 4">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_5 }}" alt="Image 5">
</div>
<div class="top_section_slider-slide_track-slide">
<img src="{{ section.settings.image_6 }}" alt="Image 6">
</div>
</div>
</div>
</div>
<style>
.top_section-container {
position: relative;
}
.top_section_box_wrapper {
padding: 32px;
}
.top_section_box {
position: relative;
margin: 0 auto;
aspect-ratio: 3 / 2;
overflow: hidden;
width: 100%;
height: calc(100vh - 120px);
}
.top_section_bg_image {
background-image: url('{{ section.settings.top_section_bg_image }}');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(3px);
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
.top_section_box_content {
padding: 32px;
width: 100%;
height: 100%;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
backdrop-filter: brightness(0.5);
}
.top_section-container-rainbow {
width: 70%;
height: 500px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: -1;
margin: 0 auto;
background: linear-gradient(90deg, #512c30 0%, #67304c 14.23%, #672f64 33.26%, #4b3971 56.49%, #35385e 81.38%, #233449 100%);
filter: blur(125px);
}
.top_section_title {
font-size: 3rem;
font-weight: 700;
letter-spacing: 0.15em;
}
.top_section_description {
font-weight: 400;
margin-top: 2rem;
opacity: 0.5;
margin-bottom: 48px;
}
{% comment %}
=====Slider====={% endcomment %}
.top_section-slider {
overflow: hidden;
width: 100%;
display: flex;
}
.top_section_slider-slide_track {
display: flex;
}
.top_section_slider-slide_track:nth-child(1) {
animation: loop 50s -25s linear infinite;
}
.top_section_slider-slide_track:nth-child(2) {
animation: loop2 50s linear infinite;
}
.top_section_slider-slide_track-slide {
width: 300px;
height: 300px;
display: flex;
justify-content: center;
align-items: center;
}
.top_section_slider-slide_track-slide img {
width: 80%;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
}
@keyframes loop {
0% {
transform: translateX(100%);
}
to {
transform: translateX(-100%);
}
}
@keyframes loop2 {
0% {
transform: translateX(0);
}
to {
transform: translateX(-200%);
}
}
@media only screen and (min-width: 600px) and(max-width: 768px) {
.top_section_title {
font-size: 2rem;
}
.top_section_description {
margin-top: 1rem;
font-size: 0.9rem;
}
}
@media only screen and (max-width: 600px) {
.top_section_title {
font-size: 2rem;
}
.top_section_description {
margin-top: 1rem;
font-size: 0.9rem;
}
.top_section_slider-slide_track-slide {
width: 150px;
height: 150px;
}
}
</style>
{% schema %}
{
"name": "Top Section",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title",
"default": "WELCOME TO THE COCOA<br />:Bitter-Sweet Kingdom"
},
{
"type": "text",
"id": "description",
"label": "Description",
"default": "\"COCOA: Bitter-Sweet Kingdom\" is a captivating tale of heroes and villains battling in a realm where chocolate, coffee, and candy coexist."
},
{
"type": "image_picker",
"id": "top_section_bg_image",
"label": "Background Image",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/top-section-bg.png"
},
{
"type": "image_picker",
"id": "image_1",
"label": "Image 1",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/chocovan+%230.png"
}, {
"type": "image_picker",
"id": "image_2",
"label": "Image 2",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/arabica+%230.png"
}, {
"type": "image_picker",
"id": "image_3",
"label": "Image 3",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/arabica.png"
}, {
"type": "image_picker",
"id": "image_4",
"label": "Image 4",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/biter+%230.png"
}, {
"type": "image_picker",
"id": "image_5",
"label": "Image 5",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/candra+%230.png"
}, {
"type": "image_picker",
"id": "image_6",
"label": "Image 6",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/chocovan+%233.png"
}
]
}
{% endschema %}

An important point is the schema tag below. Within the settings, you can define settings that you want designers and copywriters to set later, or that can be customized by various store owners when selling this theme.

These settings are called InputSettings. There are various types such as text and color_picker, so please check from the document.

{% schema %}
{
"name": "Top Section",
"settings": [
{
"type": "text",
"id": "title",
"label": "Title",
"default": "WELCOME TO THE COCOA<br />:Bitter-Sweet Kingdom"
},
{
"type": "text",
"id": "description",
"label": "Description",
"default": "\"COCOA: Bitter-Sweet Kingdom\" is a captivating tale of heroes and villains battling in a realm where chocolate, coffee, and candy coexist."
},
{
"type": "image_picker",
"id": "top_section_bg_image",
"label": "Background Image",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/top-section-bg.png"
},
{
"type": "image_picker",
"id": "image_1",
"label": "Image 1",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/chocovan+%230.png"
}, {
"type": "image_picker",
"id": "image_2",
"label": "Image 2",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/arabica+%230.png"
}, {
"type": "image_picker",
"id": "image_3",
"label": "Image 3",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/arabica.png"
}, {
"type": "image_picker",
"id": "image_4",
"label": "Image 4",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/biter+%230.png"
}, {
"type": "image_picker",
"id": "image_5",
"label": "Image 5",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/candra+%230.png"
}, {
"type": "image_picker",
"id": "image_6",
"label": "Image 6",
"default": "https://dev-cocoshop-images.s3.us-east-2.amazonaws.com/chocovan+%233.png"
}
]
}
{% endschema %}

You can access these objects as follows:

<h1 class="top_section_title">{{ section.settings.title }}</h1>
<img src="{{ section.settings.image_1 }}" alt="Image 1">

Now let’s update index.liquid.

{% section 'header' %}

<div class="main">
{% section 'top-section' %}
</div>
<style>
body {
padding: 0;
margin: 0;
background-color: #15161a;
color: white;
}
</style>

Please run cocoshop theme uploadon the command line again and check that it is reflected in the theme editor.

Step 5: Implement Other Pages

From here, you can start implementing each page using the objects, tags, and functions you’ve used so far.

For example, you can implement pages like the following:

  • collection: Collection detail screen
  • product: NFT detail screen
  • 404: Screen displayed when a user accesses a non-existent page

Please see this document for the types of pages you can currently use.

Also, please refer to the following for objects and functions:

For example, you can display the list of collections or NFTs you created in the admin panel by using objects like this:

collections object — Document

<div class="cards">
{%- for collection in **collections** -%}
<a class="card" href="{{ collection.url }}">
<img src="{{ collection.cover_image_url }}" />
<h3>{{ collection.name }}</h3>
<div>{{ collection.count }}</div>
</a>
{%- endfor -%}
</div>

collection object — Document

<div class="cards">
{%- for product in **collection.products** -%}
{% render 'item-card', collection_name: collection.name,
image_url: product.image_url,
name: product.name,
amount: product.sell.price.cryptocurrency.amount,
symbol: product.sell.price.cryptocurrency.symbol,
item_url: product.url,
collection_url: collection.url %}
{%- endfor -%}
</div>

In this way, you can develop a highly flexible UI using HTML and CSS without having to build a backend.

If you have any questions or feature requests, please ask on Discord 🚀

--

--

Ropital
NFT Application Development

CEO of CocoShop & Software engineer in blockchain space