Buy Access to Course
06.

Tailwind CSS

|

Share this awesome video!

|

I love using Tailwind for CSS. If you've never used it before, or maybe only heard of it, you might... hate it at first. That's because you use classes inside of HTML to define everything. And so your HTML can end up looking, well, a bit crazy. But give it a chance. I've absolutely fallen in love with it. And, instead of this looking ugly to me, it looks descriptive.

Tailwind Requires Building!

Tailwind isn't your traditional CSS behemoth where you download a giant CSS file and include it. Instead, Tailwind has a binary that parses all of your templates, finds the classes you're using, and then dumps a final CSS containing just those classes. So it keeps your final CSS as small as possible.

But to do this, duh duh duh! Tailwind requires a build step. And that's okay. Just because we don't have a build step for our entire JavaScript system doesn't mean we can't opt into a small one for a specific purpose.

Installing symfonycasts/tailwind-bundle

And there's a super-easy bundle to help us do this with AssetMapper. It's called symfonycasts/tailwind-bundle. Hey, I've heard of them!

Head down here, go to the documentation... and I'll copy the composer require line. Spin over and run that:

composer require symfonycasts/tailwind-bundle

This bundle will help us run the build command in the background and serve up the final file. We point it at a real CSS file, then it'll sneak in the dynamic content. You'll see.

While we're here, run:

php bin/console debug:config symfonycasts_tailwind

to see the default configuration for the bundle. By default, the file that it "builds" is assets/styles/app.css... which is great because we already have an assets/styles/app.css file!

To get things set up, run a command from the bundle:

php bin/console tailwind:init

Tip

If using the Symfony CLI, you can add a build command as a worker to be started whenever you run the Symfony web server:

# .symfony.local.yaml
workers:
    tailwind:
        cmd: ['symfony', 'console', 'tailwind:build', '--watch']

See the docs for more information.

This downloads the Tailwind binary in the background, which is awesome. That binary is standalone and doesn't require Node. It just works. The command also did two other things. First: it added the three lines needed inside of app.css:

8 lines | assets/styles/app.css
@tailwind base;
@tailwind components;
@tailwind utilities;
// ... lines 4 - 8

When this file is built, these will be replaced by the actual CSS we need. Second, this created a tailwind.config.js file:

12 lines | tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./assets/**/*.js",
"./templates/**/*.html.twig",
],
theme: {
extend: {},
},
plugins: [],
}

This tells Tailwind where to look for all the classes we'll use. This will find any classes in our JavaScript files or our templates.

To execute Tailwind, run:

php bin/console tailwind:build -w

For watch. That builds... then hangs out, waiting for future changes.

So... what did that do? Remember: we're already including app.css on our page. When we refresh, woh! It looks a bit different! The reason is, if you view the page source, and click to open the app.css file, it's full of Tailwind code! Our app.css file is no longer this exact source file! Behind the scenes, the Tailwind binary parses our templates, dumps a final version of this file, which is then returned. This file already contains a bunch of code because I filled the CRUD templates with Tailwind classes before we started the tutorial.

Using Tailwind

But let's see this in action for real. If we refresh the page, this is my h1. It's small and sad. Open templates/main/homepage.html.twig. On the h1, add class="text-3xl":

8 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<h1 class="text-3xl">Space Inviters: Plan your voyage and come in peace!</h1>
{% endblock %}

Now, refresh. It works! If that text-3xl wasn't in the app.css file before, Tailwind just added it because it's running in the background.

Pasting in The Layout

So Tailwind is working! To celebrate, let's paste in a proper layout. If you downloaded the course code, you should have a tutorial/ directory with a couple of files. Move base.html.twig into templates:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}Space Inviters!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{% block stylesheets %}
{% endblock %}
{% block javascripts %}
{{ importmap('app') }}
{% endblock %}
</head>
<body class="bg-black text-white font-mono">
<div class="container mx-auto min-h-screen flex flex-col">
<header class="my-8 px-4">
<nav class="flex items-center justify-between mb-4">
<div class="flex items-center">
<a href="{{ path('app_homepage') }}">
<img src="{{ asset('images/logo.png') }}" width="50" alt="Space Inviters Logo" >
</a>
<a href="{{ path('app_homepage') }}" class="text-xl ml-3">Space Inviters</a>
<a href="{{ path('app_voyage_index') }}" class="ml-6 hover:text-gray-400">Voyages</a>
<a href="{{ path('app_planet_index') }}" class="ml-4 hover:text-gray-400">Planets</a>
</div>
<div
class="hidden md:flex pr-10 items-center space-x-2 border-2 border-gray-900 rounded-lg p-2 bg-gray-800 text-white cursor-pointer"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M10 10m-7 0a7 7 0 1 0 14 0a7 7 0 1 0 -14 0"/><path d="M21 21l-6 -6"/></svg>
<span class="pl-2 pr-10 text-gray-500">Search Cmd+K</span>
</div>
</nav>
</header>
<!-- Make sure the main tag takes up the remaining height -->
<main class="flex-grow">{% block body %}{% endblock %}</main>
<!-- Footer -->
<footer class="py-4 mt-6 bg-gray-800 text-center">
<div class="text-sm">
With <svg class="inline-block w-4 h-4 text-red-600 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M10 3.22l-.61-.6a5.5 5.5 0 00-7.78 7.78l7.39 7.4 7.39-7.4a5.5 5.5 0 00-7.78-7.78l-.61.61z"/></svg> from Symfonycasts.
</div>
</footer>
</div>
</body>
</html>

And these other two go into the main/ directory:

{% extends 'base.html.twig' %}
{% block title %}Space Inviters!{% endblock %}
{% block body %}
<div class="flex">
<aside class="hidden md:block md:w-64 bg-gray-900 px-2 py-6">
<h2 class="text-xl text-white font-semibold mb-6 px-2">Planets</h2>
<div>
{{ include('main/_planet_list.html.twig') }}
</div>
</aside>
<section class="flex-1 ml-10">
<form
method="GET"
action="{{ path('app_homepage') }}"
class="mb-6 flex justify-between"
>
<input
type="search"
name="query"
value="{{ app.request.query.get('query') }}"
aria-label="Search voyages"
placeholder="Search voyages"
class="w-1/3 px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400"
>
<div class="whitespace-nowrap m-2 mr-4">{{ voyages|length }} results</div>
</form>
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
<thead>
<tr>
<th class="text-left py-2">Purpose</th>
<th class="text-left py-2 pr-4">Planet</th>
<th class="text-left py-2">Departing</th>
</tr>
</thead>
<tbody>
{% for voyage in voyages %}
<tr class="border-b border-gray-700 {% if loop.index is odd %} bg-gray-800 {% else %} bg-gray-700 {% endif %}">
<td class="p-4">{{ voyage.purpose }}</td>
<td class="px-2 whitespace-nowrap">
<img
src="{{ asset('images/'~voyage.planet.imageFilename) }}"
alt="Image of {{ voyage.planet.name }}"
class="inline-block w-8 h-8 rounded-full bg-gray-600 ml-2"
>
</td>
<td class="px-2 whitespace-nowrap">{{ voyage.leaveAt|date('Y-m-d') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="flex items-center mt-6 space-x-4">
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Previous</a>
<a href="#" class="block py-2 px-4 bg-gray-700 text-white rounded hover:bg-gray-600">Next</a>
</div>
</section>
</div>
{% endblock %}

<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
<div class="pr-3">
<span class="text-white">{{ planet.name }}</span>
<span class="block text-gray-400 text-sm">{{ planet.lightYearsFromEarth|round|number_format }} light years</span>
</div>
<img
class="flex-shrink-0 w-8 h-8 bg-gray-600 rounded-full group-hover:bg-gray-500 transition duration-300 ease-in-out"
src="#"
alt="Image of {{ planet.name }}"
>
</div>
</a>
</li>
{% endfor %}
</ul>

Refresh. Huh, no difference. That's because, at least on a Mac, because I moved and overwrote those files, Twig didn't notice that they were updated... so its cache is out-of-date.

Open a fresh terminal tab and run:

php bin/console cache:clear

Then... woo! Welcome to Space Inviters! Styled up and ready to go! But there's nothing special about the new templates. We do have a list of voyages... but it's all boring, normal Twig code with Tailwind classes.

Referencing Assets Dynamically

We do have some broken planet images though. To fix those, go into the tutorial/assets/ directory... and move all of those planets into assets/images/. Delete the tutorial/ folder.

That broken img tag comes from the _planet_list.html.twig partial. Here it is:

22 lines | templates/main/_planet_list.html.twig
<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
// ... lines 8 - 11
<img
// ... line 13
src="#"
// ... line 15
>
</div>
</a>
</li>
{% endfor %}
</ul>

I left it for us to finish! How nice of me! We know that we can do {{ assets() }} then something like images/planets-1.png. That would work. But this time, the planet-1.png part is a dynamic property on the Planet entity. So, instead say ~ then planet.imageFilename:

22 lines | templates/main/_planet_list.html.twig
<ul>
{% for planet in planets %}
<li class="mb-4 group">
<a href="{{ path('app_planet_show', {
'id': planet.id,
}) }}" class="block transform transition duration-300 ease-in-out hover:translate-x-1 hover:bg-gray-700 rounded">
<div class="flex justify-between items-start p-2">
// ... lines 8 - 11
<img
// ... line 13
src="{{ asset('images/'~planet.imageFilename) }}"
// ... line 15
>
</div>
</a>
</li>
{% endfor %}
</ul>

And... pretty! Yea, I know that's not what Earth and Saturn look like - I've got some randomness in my fixtures - but they're fun to look at!

Using KnpTimeBundle

Since day 6 is the "making everything look awesome day", let's do two more things. To start, I don't love this date. It's boring! I want a cool looking date.

So, install one of my favorite bundles:

composer require knplabs/knp-time-bundle

This gives us a handy ago filter. So as soon as this finishes, spin over and open homepage.html.twig. Search for leaveAt, here we go. Replace that date filter with ago:

63 lines | templates/main/homepage.html.twig
// ... lines 1 - 4
{% block body %}
<div class="flex">
// ... lines 7 - 13
<section class="flex-1 ml-10">
// ... lines 15 - 29
<div class="bg-gray-800 p-4 rounded">
<table class="w-full text-white">
// ... lines 32 - 38
<tbody>
{% for voyage in voyages %}
<tr class="border-b border-gray-700 {% if loop.index is odd %} bg-gray-800 {% else %} bg-gray-700 {% endif %}">
// ... lines 42 - 49
<td class="px-2 whitespace-nowrap">{{ voyage.leaveAt|ago }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
// ... lines 56 - 59
</section>
</div>
{% endblock %}

And... much cooler!

What else? Go check out the CRUD areas. These were generated via MakerBundle... but... I did preload them with Tailwind classes so they look good. Wow, there is a lot of celebrating right now. I'm not complaining.

But... if you go to a form, it looks terrible! Why? The form markup comes from Symfony's form theme... which outputs the fields without Tailwind classes.

Flowbite for Tailwind Examples

So what do we do? Do we need to create a form theme? Fortunately, no. One of the great things about Tailwind is there's an entire ecosystem set up around it. For example, Flowbite is a site with a mixture of open source examples and pro, paid features. On the open source side of things, you can, for example, find an "Alert" page with different markup to make great-looking alerts. And, you don't need to install anything with Flowbite. These classes are all native Tailwind. You can copy this markup into your project and refresh. Nothing special. And I love it.

Tip

Flowbite does also come with some JavaScript & a Tailwind Plugin. Check out the bonus chapter for the details!

This also has a forms section where it shows how we can make forms look really nice. In theory, if we could make our forms output like this, they would look great.

Adding a Tailwind Form Theme

And fortunately, there's a bundle that can help us. It's called tales-from-a-dev/flowbite-bundle. Click "Installation" and copy the composer require line. Then run it:

composer require tales-from-a-dev/flowbite-bundle

We're getting a lot of stuff installed today! This asks if we want to install the contrib recipe. Let's say yes, permanently. Cool!

Back on the installation instructions, we don't need to register the bundle - that happens automatically - but we do need to copy this line for the tailwind configuration file.

Open up tailwind.config.js, and paste that:

13 lines | tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
// ... lines 4 - 5
"./vendor/tales-from-a-dev/flowbite-bundle/templates/**/*.html.twig",
],
// ... lines 8 - 11
}

This bundle comes with its own form theme template with its own Tailwind classes. So we want to make sure that Tailwind sees those and outputs the CSS for them.

The last step over on the docs is to tell our system to use this form theme by default. This happens over in config/packages/twig.yaml. I'll paste... then fix the indentation:

Tip

Starting in version 0.4.0, use @TalesFromADevFlowbite/form/default.html.twig.

8 lines | config/packages/twig.yaml
twig:
// ... line 2
form_themes: ['@Flowbite/form/default.html.twig']
// ... lines 4 - 8

That's it. Go back, refresh and eureka! In just over 10 minutes, we installed Tailwind and started using it everywhere.

Tomorrow, we'll turn back to JavaScript and leverage Stimulus to write reliable JavaScript code that we can love.