Style Board Game Cards with Tailwind CSS

Alex Aguilar, Partner, Software Engineer Alex Aguilar Craft CMS

In the previous lessons, we created the Board Games section and then imported our favorite board games using the FeedMe plugin. So far we’ve only worked in the Craft admin panel, that changes today when we update the homepage Twig template to display our board games & style it with Tailwind CSS.

Mockup of Board Games row #

Here’s a screenshot of a board game row displaying 3 cards.

💡 Check it out on the Tailwind Play playground.

Update Homepage to list board game titles #

Before setting up Tailwind, let’s get familiar with updating the homepage. We’ll modify the default index.twig template to list our board game titles.

  1. Open templates/index.twig in your favorite code editor.1
  2. Scroll down until you find the H2 with “Popular Resources” and replace it with “Favorite Board Games”.
  3. We’re going to grab ALL or our board game data and put into a variable boardgames. Copy & paste the snippet below, put it right above the H2.

     {% set boardgames = craft.entries.section('boardGames').all() %}
    
  4. Replace all the <li> rows with the following.

     {% for boardgame in boardgames %}
       {% set image = boardgame.boardGameImage.one() %}
       <li>
         <a href="{{image.getUrl}}" target="_blank">{{ boardgame.title }}</a><br>
         <small>{{ boardgame.boardGameCategory.one()}}</small>
       </li>
     {% endfor %}
    
💡 You can look up the field handles in Settings> Fields.

Visit the homepage and you’ll see a list of board games like this:

https://cdn.eaglepeakweb.com/img/projects/blog/First-board-game-created.jpg?mtime=20200917134817&focal=none

So we’re looping through each individual boardgame and

  • Displaying the Title
  • Linking the Title to the board game’s cover image
  • Displaying the Category below the Title

Notice that Twig has 2 types of curly braces.

  1. {{ }} print this

  2. {% %} do this like set a variable, start a loop or check a condition

Also notice the .one() method for the image & category. Since a board game could have multiple images or categories, we only want the first value.

Simple Tailwind CSS Setup #

You could easily spend half a day setting up a front-end build chain with Gulp, webpack, etc. So our setup is deliberately “simple” 🤔 to get us going.2

Node.js #

You’ll need to have Node.js installed locally. So go to the Node.js homepage & download the LTS version for your OS.

Lando - con­fig­ure fron­t-end tool­ing #

If you’re using Lando for local development, then you can add front-end tooling by updating .lando.yml

name: boardgames
recipe: lamp
config:
  webroot: web
  php: '7.4'
  database: mariadb:10.3
services:
  database:
    type: mariadb:10.3
    portforward: 3310
    creds:
      user: homestead
      password: secret
      database: boardgames
  node:
    type: node
    build:
      - npm install tailwindcss
      - npm install laravel-mix
tooling:
  npm:
    service: node
  npx:
    service: node
  node:
    service: node

We’ve added Node.js in a new container to our project. And we’re also installing the tailwindcss & laravel-mix packages.

You’ll then rebuild your Lando containers. lando rebuild

alexagui@alexMBP ~/Code/craft/boardgames (develop/tailwind-homepage) $ lando rebuild
? Are you sure you want to rebuild? (y/N)Yes
Rising anew like a fire phoenix from the ashes! Rebuilding app...
Killing boardgames_appserver_1 ... done
Killing boardgames_database_1  ... done
Going to remove boardgames_appserver_1, boardgames_database_1
Removing boardgames_appserver_1 ... done
Removing boardgames_database_1  ... done
Pulling appserver ... extracting (0.3%)
Pulling node      ... downloading (55.6%)
Pulling database  ... done

Install Tailwind #

💡 If you’re not using Lando then just omit lando from the commands below .
  1. lando npm init -y
    This will create a package.json file in your project root.
  2. lando npx tailwindcss init

    alexagui@alexMBP ~/Code/craft/boardgames (develop/tailwind-homepage) $ lando npx tailwindcss init
    
    tailwindcss 1.9.5
    
    ✅ Created Tailwind config file: tailwind.config.js
    
  3. We’re going to use Laravel Mix to compile our CSS. If you’re using Lando then it should already be installed when we updated .lando.yml above & rebuilt the containers. If you’re not using Lando then you need to install Laravel Mix npm install laravel-mix

  4. Next we setup our source CSS file. Let’s create it under a new folder source/css/app.css
    mkdir -p source/css && touch $_/app.css

  5. Put the following inside app.css

    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    
  6. Next, in the project root, we create a webpack.mix.js and put the following in it.

    const mix = require('laravel-mix');
    const tailwindcss = require('tailwindcss');
    mix.postCss('source/css/app.css', 'web/css/app.css', [
         tailwindcss('tailwind.config.js')
     ]
    );
    
  7. Modify package.json by replacing the “scripts” section with this:

    "scripts": {
     "dev": "npm run development",
     "development": "NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
     "watch": "npm run development -- --watch",
     "watch-poll": "npm run watch -- --watch-poll",
     "hot": "NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
     "prod": "npm run production",
     "production": "NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
    },
    
  8. Build the CSS by running lando npm run dev

    $ lando npm run dev
    > app@1.0.0 dev /app  
    npm run development
    > app@1.0.0 development /app
    > NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js
    ...
    🛫 lots of stuff flying by 🛬
    ...
    DONE  Compiled successfully in 11408ms          9:03:32 PM
    
           Asset      Size  Chunks             Chunk Names
    web/css/app.css  2.31 MiB     mix  [emitted]  mix
    

    Notice that our CSS file has been built at web/css/app.css

Update Homepage to display styled cards #

Replace default styling #

  1. Open templates/index.twig.
  2. Between the last <meta> tag and before the <style> tag add:

     <link rel="stylesheet" href="/css/app.css">
    
  3. Remove all of the <style> section.

  4. Replace the contents of the <body> tags with:

      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
     <div class="grid grid-cols-4 gap-1 rounded bg-white text-black overflow-hidden border border-gray-400 bg-white rounded-b px-4 justify-between leading-normal">
       <div class="col-span-2 pr-2">
      <img class="w-full h-full object-cover" src="https://cdn.eaglepeakweb.com/img/projects/blog/Race-for-the-Galaxy.jpg" title="Race for the Galaxy">
    
       </div>
       <div class="col-span-2">
         <p class="text-sm text-gray-600 flex items-center pt-4">
           <span class="inline-block bg-red-200 text-red-800 rounded-full px-2 text-xs font-semibold tracking-wide">Strategy</span>
         </p>
         <div class="text-gray-900 font-bold text-xl mb-2">Race for the Galaxy</div>
         <div class="flex items-center">
           <svg class="fill-current text-gray-500 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2zm5-10a1 1 0 0 1-1 1h-6a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1z"/>
           </svg>
           <span class="ml-2 mr-2 text-gray-700 text-sm">2</span>
    
           <svg class="fill-current text-gray-500 w-4 h-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M19 10h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0v-2h-2a1 1 0 0 1 0-2h2V8a1 1 0 0 1 2 0v2zM9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2z"/>
           </svg>
           <span class="ml-2 mr-2 text-gray-700 text-sm">4</span>
    
           <svg class="fill-current text-gray-500 w-3 w-4 h-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2zm-1.3-10.7l1.3 1.29 3.3-3.3a1 1 0 0 1 1.4 1.42l-4 4a1 1 0 0 1-1.4 0l-2-2a1 1 0 0 1 1.4-1.42z"/>
           </svg>
           <span class="ml-2 mr-2 text-green-600 text-sm">2</span>
         </div>
    
         <div class="flex items-center pt-8">
           <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm1-8.41l2.54 2.53a1 1 0 0 1-1.42 1.42L11.3 12.7A1 1 0 0 1 11 12V8a1 1 0 0 1 2 0v3.59z"/>
           </svg>
           <span class="ml-1 text-gray-700 leading-none text-sm">30-60 minutes</span>
         </div>
    
         <div class="mt-2 flex items-center">
           <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M20 22H4a2 2 0 0 1-2-2v-8c0-1.1.9-2 2-2h4V8c0-1.1.9-2 2-2h4V4c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2zM14 8h-4v12h4V8zm-6 4H4v8h4v-8zm8-8v16h4V4h-4z"/>
           </svg>
           <span class="ml-1 text-yellow-700 leading-none text-sm">Medium</span>
         </div>
         <div class="mt-2 flex items-center">
           <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
             <path class="heroicon-ui" d="M4.06 13a8 8 0 0 0 5.18 6.51A18.5 18.5 0 0 1 8.02 13H4.06zm0-2h3.96a18.5 18.5 0 0 1 1.22-6.51A8 8 0 0 0 4.06 11zm15.88 0a8 8 0 0 0-5.18-6.51A18.5 18.5 0 0 1 15.98 11h3.96zm0 2h-3.96a18.5 18.5 0 0 1-1.22 6.51A8 8 0 0 0 19.94 13zm-9.92 0c.16 3.95 1.23 7 1.98 7s1.82-3.05 1.98-7h-3.96zm0-2h3.96c-.16-3.95-1.23-7-1.98-7s-1.82 3.05-1.98 7zM12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20z"/>
           </svg>
           <span class="ml-1 text-gray-700 leading-none text-sm">
             <a href="https://boardgamearena.com/gamepanel?game=raceforthegalaxy">Play online</a>
           </span>
         </div>
       </div>
     </div>
      </div>
    

Eek! That’s a lot of code and verbosity is a complaint about Tailwind CSS. But I’ve found that its utility-first approach makes it fairly easy for a front-end challenged developer like myself to build decent looking pages.

If you refresh the page you’ll see just one card being displayed on the homepage.

We just hard-coded a sample card to ensure Tailwind CSS is configured correctly. Now let’s update so the cards are served dynamically.

Dynamically display board game cards #

  1. Once again let’s grab all the board game data. Put the following snippet at very top of the templates/index.twig template.

    {% set boardgames = craft.entries.section('boardGames').all() %}
    
  2. Next we’ll start our loop. Look for this DIV

    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    

    and right below it add:

    {% for boardgame in boardgames %}
      {% set image = boardgame.boardGameImage.one() %}
      {% set category = boardgame.boardGameCategory.one() %}
    

    Don’t forget to close the loop.

       </div>
     {% endfor %}
    </div>
    </body>
    </html>
    

    Take a quick peek at the homepage and it should look like this:

  3. Let’s update the template with the appropriate board game Craft field handles. Below is the fully updated code. Replace the contents of the <body> tags.

    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
     {% for boardgame in boardgames %}
         {% set image = boardgame.boardGameImage.one() %}
         {% set category = boardgame.boardGameCategory.one() %}
         <div class="grid grid-cols-4 gap-1 rounded bg-white text-black overflow-hidden border border-gray-400 bg-white rounded-b px-4 justify-between leading-normal">
             <div class="col-span-2 pr-2">
    <a href="{{image.getUrl}}"><img class="w-full h-full object-cover" src="{{image.getUrl}}" title=">{{ boardgame.title }}"></a>
             </div>
             <div class="col-span-2">
                 <p class="text-sm text-gray-600 flex items-center pt-4">
                     <span class="inline-block bg-red-200 text-red-800 rounded-full px-2 text-xs font-semibold tracking-wide">{{category}}</span>
                 </p>
    <div class="text-gray-900 font-bold text-xl mb-2">{{ boardgame.title }}</div>
    
                 <div class="flex items-center">
                     <svg class="fill-current text-gray-500 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2zm5-10a1 1 0 0 1-1 1h-6a1 1 0 0 1 0-2h6a1 1 0 0 1 1 1z"/>
                     </svg>
    <span class="ml-2 mr-2 text-gray-700 text-sm">{{boardgame.minNumPlayers}}</span>
    
                     <svg class="fill-current text-gray-500 w-4 h-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M19 10h2a1 1 0 0 1 0 2h-2v2a1 1 0 0 1-2 0v-2h-2a1 1 0 0 1 0-2h2V8a1 1 0 0 1 2 0v2zM9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2z"/>
                     </svg>
                     <span class="ml-2 mr-2 text-gray-700 text-sm">{{boardgame.maxNumPlayers}}</span>
    
                     <svg class="fill-current text-gray-500 w-3 w-4 h-4 ml-2" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M9 12A5 5 0 1 1 9 2a5 5 0 0 1 0 10zm0-2a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm8 11a1 1 0 0 1-2 0v-2a3 3 0 0 0-3-3H7a3 3 0 0 0-3 3v2a1 1 0 0 1-2 0v-2a5 5 0 0 1 5-5h5a5 5 0 0 1 5 5v2zm-1.3-10.7l1.3 1.29 3.3-3.3a1 1 0 0 1 1.4 1.42l-4 4a1 1 0 0 1-1.4 0l-2-2a1 1 0 0 1 1.4-1.42z"/>
                     </svg>
                     <span class="ml-2 mr-2 text-green-600 text-sm">{{boardgame.bestNumPlayers}}</span>
                 </div>
    
                 <div class="flex items-center pt-8">
                     <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm1-8.41l2.54 2.53a1 1 0 0 1-1.42 1.42L11.3 12.7A1 1 0 0 1 11 12V8a1 1 0 0 1 2 0v3.59z"/>
                     </svg>
                     <span class="ml-1 text-gray-700 leading-none text-sm">{{boardgame.minDuration}}-{{boardgame.maxDuration}} minutes</span>
                 </div>
    
                 <div class="mt-2 flex items-center">
                     <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M20 22H4a2 2 0 0 1-2-2v-8c0-1.1.9-2 2-2h4V8c0-1.1.9-2 2-2h4V4c0-1.1.9-2 2-2h4a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2zM14 8h-4v12h4V8zm-6 4H4v8h4v-8zm8-8v16h4V4h-4z"/>
                     </svg>
                     <span class="ml-1 text-yellow-700 leading-none text-sm">{{boardgame.difficulty.label}}</span>
                 </div>
                 {% if boardgame.playOnline|length %}
                 <div class="mt-2 flex items-center">
                     <svg class="fill-current text-gray-500 w-3 w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                         <path class="heroicon-ui" d="M4.06 13a8 8 0 0 0 5.18 6.51A18.5 18.5 0 0 1 8.02 13H4.06zm0-2h3.96a18.5 18.5 0 0 1 1.22-6.51A8 8 0 0 0 4.06 11zm15.88 0a8 8 0 0 0-5.18-6.51A18.5 18.5 0 0 1 15.98 11h3.96zm0 2h-3.96a18.5 18.5 0 0 1-1.22 6.51A8 8 0 0 0 19.94 13zm-9.92 0c.16 3.95 1.23 7 1.98 7s1.82-3.05 1.98-7h-3.96zm0-2h3.96c-.16-3.95-1.23-7-1.98-7s-1.82 3.05-1.98 7zM12 22a10 10 0 1 1 0-20 10 10 0 0 1 0 20z"/>
                     </svg>
                     <span class="ml-1 text-gray-700 leading-none text-sm">
                         <a href="{{boardgame.playOnline}}">Play online</a>
                     </span>
                 </div>
                 {% endif %} 
             </div>
         </div>
     {% endfor %}
    </div>
    
☝️ Note that we check if the playOnline field is present before displaying a Play online link.

Refresh the homepage and it should look similar to:

Wrapping Up #

In this lesson, we updated the homepage to display board games cards & styled them with Tailwind CSS. But the approach we took of 1 HUGE template isn’t best practice. Ideally, we should have a layout template and the cards should be an include file. You want to keep code in smaller more manageable pieces instead of huge blobs.

It also bugs me that there’s no margin between the cards and edge of browser window. And that the category & difficulty all have the same color. I’d like the difficulties to display in appropriate colors ex: Easy and Hard.

In the next lesson, we’ll refactor our template & polish up these styling issues.

✨ This series is in development. Sign up or follow me @AlexAguilar18 to learn when new lessons are released.

  1. Like the excellent Visual Studio Code, a free cross-platform editor by Microsoft of all people.

  2. Thanks to @ademers gist on setting up Tailwind CSS in Craft CMS 3.


Alex Aguilar, Partner, Software Engineer

Alex Aguilar

Partner, Software Engineer