Tailwind CSS 4 with Grails 8
Wire Tailwind CSS 4 into a Grails 8 GSP application with class-based dark mode and a small @apply component layer, all driven from the Gradle build.
Authors: James Fredley
Grails Version: 8
1 Getting Started
In this guide you will wire Tailwind CSS 4 into a fresh Apache Grails 8 application so every GSP view can be styled with utility classes, with class-based dark mode and a small @apply component layer for the patterns that repeat.
The end state stays close to the Grails 8 web starter. GSPs still render through the standard layout tags, the asset pipeline still serves application.css, and Gradle owns the extra Tailwind compile step.
This guide targets Apache Grails 8.
Files touched in this chapter:
-
None
1.1 What You Will Build
By the end of the guide your application will:
-
Render
grails-app/views/layouts/main.gspandgrails-app/views/index.gspwith Tailwind 4 utility classes instead of the default Bootstrap 5 classes the starter ships with. -
Compile
src/main/css/input.css(the Tailwind entry point) intograils-app/assets/stylesheets/app.cssvia atailwindBuildGradle task that runs beforeprocessResourcesandcompileGroovy. The asset pipeline then bundles, fingerprints, and gzips that file into theapplication.cssbundle the layout serves. -
Toggle dark mode at runtime by adding the
darkclass on<html>, persisting the choice tolocalStorageso reloads do not flash light-to-dark. -
Expose three reusable component classes (
btn-primary,card,nav-link) defined in the@layer componentsblock ofinput.cssand used directly from GSP markup.
The result is a small but production-shaped baseline you can copy into any Grails 8 app that still wants the GSP view layer but a modern utility-first CSS pipeline.
Files touched in this chapter:
-
None
1.2 What You Will Need
To complete this guide you will need:
-
JDK 21
-
Node.js 20+
-
npm 10+
-
A Bash, PowerShell, or
cmdshell capable of running the included Gradle wrapper (./gradleworgradlew.bat) -
About 30 minutes
Files touched in this chapter:
-
None
1.3 How to Complete the Guide
You can either type the code in this guide as you read or skip ahead and clone the finished sample app from the upstream repository:
git clone -b grails8 https://github.com/grails-guides/grails-tailwindcss.git
cd grails-tailwindcss/complete
./gradlew bootRun
The repository contains two top-level directories:
-
initial/- the starting Grails 8 project, generated from start.grails.org with no customisations applied. -
complete/- the same project with all Tailwind wiring from this guide already in place.
Each chapter ends with the file paths it touches relative to the project root, so you can match what you typed against the complete/ reference.
Files touched in this chapter:
-
None
2 Creating the Application
Generate a fresh Apache Grails 8 web application from start.grails.org. This is the same forge that produced the initial/ tree in the upstream repository.
Use the web profile. That keeps you on the same starter layout, index page, asset-pipeline manifest, and dependency set that Grails 8 ships from grails-profiles/web.
Files touched in this chapter:
-
build.gradle -
grails-app/views/layouts/main.gsp -
grails-app/views/index.gsp
2.1 Download a Grails 8 Starter
Open start.grails.org, pick the web profile, name the application tailwind, set the package to example, choose JDK 21, and download the generated zip. Unzip it and cd into the tailwind directory.
Verify it boots before you go further:
./gradlew bootRun
Open http://localhost:8080/ in a browser and you should see the default Grails landing page. Stop the application with Ctrl+C once you have confirmed it runs.
|
The forge’s default |
Files touched in this chapter:
-
build.gradle -
settings.gradle -
grails-app/views/layouts/main.gsp -
grails-app/views/index.gsp
3 Installing Tailwind CSS 4
Tailwind 4 ships as the npm package tailwindcss, with a separate @tailwindcss/cli package for the standalone command-line compiler that we will drive from Gradle. Add a small package.json at the project root:
{
"name": "grails-tailwindcss",
"version": "1.0.0",
"private": true,
"description": "Tailwind CSS 4 build wiring for the grails-tailwindcss/v8 sample app.",
"engines": {
"node": ">=20",
"npm": ">=10"
},
"scripts": {
"build": "tailwindcss -i src/main/css/input.css -o grails-app/assets/stylesheets/app.css --minify",
"watch": "tailwindcss -i src/main/css/input.css -o grails-app/assets/stylesheets/app.css --watch"
},
"devDependencies": {
"@tailwindcss/cli": "^4.0.0",
"tailwindcss": "^4.0.0"
}
}
Then run npm install once to populate node_modules/ and create package-lock.json:
npm install
The build and watch scripts call the tailwindcss binary that @tailwindcss/cli exposes, while the Gradle task you will add in a later chapter runs the same compiler through npx @tailwindcss/cli. That keeps the npm side and Gradle side aligned.
Files touched in this chapter:
-
package.json -
package-lock.json
4 Configuring Tailwind for Grails
Tailwind 4 introduced a CSS-first configuration model. Most projects no longer need a tailwind.config.js file at all. Theme tokens, content sources, and custom variants are declared in your CSS entry point with the @theme, @source, and @custom-variant at-rules.
For a Grails app that means three knobs to set:
-
Where to scan for class names. Tailwind 4’s scanner does not know about
.gspfiles out of the box, and it is conservative about reading utility-class string literals from.groovysource. Both have to be added with explicit@sourcedirectives. -
How to opt into class-based dark mode. The default dark variant follows
prefers-color-scheme. To let the user pick a theme and persist it, override the variant sodark:utilities apply when an ancestor has thedarkclass. -
Reusable component classes (optional). Tailwind 4 adds
@utilityfor custom utilities, but a small@layer componentsblock is still a good fit when you want a few named classes that read well in GSPs.
All three live in src/main/css/input.css, which we will write next.
Files touched in this chapter:
-
src/main/css/input.css
4.1 The input.css Entry Point
Create the Tailwind entry point at src/main/css/input.css:
/*
* Tailwind CSS 4 entry point.
*
* Tailwind 4 uses CSS-first configuration: the @import below pulls in the
* framework, @source tells the JIT scanner where to find class names, and
* @custom-variant lets us opt into class-based dark mode without a separate
* tailwind.config.js. See guides/grails-tailwindcss/v8/guide/configureTailwind.adoc.
*/
@import "tailwindcss";
/*
* Where to scan for class names. Tailwind 4 auto-detects most file types,
* but does not know about .gsp out of the box and is conservative about
* scanning .groovy files for utility-class string literals (e.g. classes
* passed via tag-attribute composition in controllers or tag libraries).
*/
@source "../../../grails-app/views";
@source "../../../grails-app/controllers";
@source "../../../grails-app/services";
@source "../../../grails-app/taglib";
@source "../../../src/main/groovy";
/*
* Dark mode via a class on <html>. Toggled at runtime in main.gsp.
* The default Tailwind 4 dark variant follows prefers-color-scheme; this
* override lets a user persist their choice across sessions.
*/
@custom-variant dark (&:where(.dark, .dark *));
/*
* A small component layer: classes you can spell once and reuse across
* GSPs without the full utility chain. Good fit for buttons, cards, and
* form-control wrappers that the Grails Fields plugin will eventually
* render around your inputs.
*/
@layer components {
.btn-primary {
@apply inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-offset-gray-900;
}
.card {
@apply rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800;
}
.nav-link {
@apply text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white;
}
}[]
Four things in this file are worth highlighting:
-
@import "tailwindcss";pulls in the framework. In v3 you wrote three separate@tailwind base; @tailwind components; @tailwind utilities;directives; v4 collapses all three into the single@import. -
The
@sourcedirectives tell the scanner exactly where to look. The starter the forge produces puts views undergrails-app/views, controllers undergrails-app/controllers, services undergrails-app/services, tag libraries undergrails-app/taglib, and helper code undersrc/main/groovy. -
The
@custom-variant dark (&:where(.dark, .dark *));line redefinesdark:so it applies whenever an ancestor (typically<html>) carries thedarkclass. Without this override the v4 default usesprefers-color-scheme, which honours the OS theme but does not let the user override it. -
The
@layer componentsblock at the bottom defines.btn-primary,.card, and.nav-link. These are referenced frommain.gspandindex.gspin later chapters.
Files touched in this chapter:
-
src/main/css/input.css
5 Wiring Tailwind into the Gradle Build
A small block at the bottom of build.gradle is everything the build needs:
def npxCmd = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'npx.cmd' : 'npx'
def npmCmd = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'npm.cmd' : 'npm'
def npmInstall = tasks.register('npmInstall', Exec) {
description = 'Install npm dependencies for Tailwind CSS 4'
group = 'tailwind'
inputs.files('package.json', 'package-lock.json')
outputs.dir('node_modules')
commandLine npmCmd, 'install', '--no-fund', '--no-audit'
}
def tailwindBuild = tasks.register('tailwindBuild', Exec) {
description = 'Compile src/main/css/input.css into grails-app/assets/stylesheets/app.css'
group = 'tailwind'
dependsOn npmInstall
inputs.files('package.json', 'package-lock.json', 'src/main/css/input.css')
inputs.dir('grails-app/views')
inputs.dir('grails-app/controllers')
inputs.dir('grails-app/services')
inputs.dir('grails-app/taglib')
inputs.dir('src/main/groovy')
outputs.file('grails-app/assets/stylesheets/app.css')
commandLine npxCmd, '@tailwindcss/cli',
'-i', 'src/main/css/input.css',
'-o', 'grails-app/assets/stylesheets/app.css',
'--minify'
}
tasks.named('processResources').configure { dependsOn tailwindBuild }
tasks.named('compileGroovy').configure { dependsOn tailwindBuild }
And the asset manifest becomes a one-line include of the generated Tailwind output:
/*
* Asset-pipeline manifest. Pull the Tailwind CLI output into the canonical
* application.css bundle the layout already serves.
*
*= require app
*/
Two Exec tasks do the work:
-
npmInstallrunsnpm installand caches its result. Thenode_modules/output plus thepackage.jsonandpackage-lock.jsoninputs mean Gradle reruns it only when the Node dependency graph changes. -
tailwindBuildcallsnpx @tailwindcss/cliwith the same-iand-opaths used by the npm scripts. Itsinputscover the entry CSS and every directory that the@sourcedirectives scan.
The two tasks.named lines hook tailwindBuild into the standard Grails build graph so plain ./gradlew bootRun, assemble, or bootJar regenerates app.css before resources and Groovy sources compile. For iterative work without Gradle in the loop, run npm run watch in a second shell.
The compiled app.css lands inside grails-app/assets/stylesheets/, which is the asset pipeline’s source directory. The layout still asks for application.css via <asset:stylesheet>, but the manifest now pulls in the Tailwind output instead of the starter’s Bootstrap CSS. app.css itself is regenerated on every build, so it is git-ignored.
Files touched in this chapter:
-
build.gradle -
grails-app/assets/stylesheets/application.css
6 Applying Tailwind to the GSP Layout
With the Tailwind pipeline in place, replace the layout markup. The starter ships a Bootstrap 5 layout in grails-app/views/layouts/main.gsp; rewrite it with Tailwind utilities and a small flex-based navbar:
<!doctype html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><g:layoutTitle default="Grails"/></title>
<asset:link rel="icon" href="favicon.ico" type="image/x-ico"/>
<asset:stylesheet src="application.css"/>
<%-- Pre-paint dark-mode resolution: read the user's saved choice (or
system preference) and apply the `dark` class on <html> BEFORE
the body renders. This avoids the light-to-dark flash on reload. --%>
<script>
(function () {
var saved = localStorage.getItem('theme');
var prefers = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (saved === 'dark' || (!saved && prefers)) {
document.documentElement.classList.add('dark');
}
})();
</script>
<g:layoutHead/>
</head>
<body class="h-full bg-gray-50 text-gray-900 antialiased dark:bg-gray-900 dark:text-gray-100">
<nav class="border-b border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-800">
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
<a class="flex items-center gap-2" href="${request.contextPath}/">
<asset:image class="h-8 w-auto" src="grails.svg" alt="Grails Logo"/>
<span class="text-base font-semibold">Grails + Tailwind CSS</span>
</a>
<div class="flex items-center gap-6">
<a href="https://grails.apache.org/docs/" class="nav-link" target="_blank" rel="noopener">Docs</a>
<a href="https://grails.apache.org/community.html" class="nav-link" target="_blank" rel="noopener">Community</a>
<button id="theme-toggle"
type="button"
aria-label="Toggle dark mode"
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600">
<span aria-hidden="true" class="dark:hidden">☼</span>
<span aria-hidden="true" class="hidden dark:inline">☽</span>
</button>
</div>
</div>
</nav>
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<g:layoutBody/>
</div>
<footer class="mt-12 border-t border-gray-200 bg-white py-8 dark:border-gray-700 dark:bg-gray-800">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<p class="text-sm text-gray-500 dark:text-gray-400">
Built with Apache Grails 8 and Tailwind CSS 4. See the
<a class="font-medium text-blue-600 hover:underline dark:text-blue-400"
href="https://grails.apache.org/guides/grails-tailwindcss/8/guide/index.html">
guide
</a> for the full source.
</p>
</div>
</footer>
<script>
document.getElementById('theme-toggle').addEventListener('click', function () {
var html = document.documentElement;
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
});
</script>
<asset:javascript src="application.js"/>
</body>
</html>[]
A few patterns are worth pointing at:
-
The
<asset:stylesheet src="application.css"/>taglib resolves to the bundled, fingerprintedapplication.cssURL at runtime. The manifest you edited in the previous chapter is what tells the pipeline to include the compiled Tailwindapp.cssin that bundle. The layout never referencesapp.cssdirectly. -
The layout keeps the starter’s
favicon.icolink andapplication.jsinclude. That means the page still uses the stock asset-pipeline conventions even though the CSS is now Tailwind-driven. -
The outer
<body>carriesbg-gray-50 dark:bg-gray-900so the colour scheme flips at the top level. -
The navbar uses
mx-auto max-w-7xlplus a flex row, replacing the Bootstrapnavbarcomponent with a hand-rolled equivalent that is easier to customise. -
The footer link is the
text-blue-600 hover:underline dark:text-blue-400triplet that you will repeat all over the app once you commit to the utility-first approach.
The welcome page itself follows the same recipe:
<%@ page import="grails.util.Environment"%>
<%@ page import="org.springframework.boot.SpringBootVersion"%>
<%@ page import="org.springframework.core.SpringVersion"%>
<g:set var="pluginManager" bean="pluginManager"/>
<g:set var="numControllers" value="${grailsApplication.controllerClasses.size()}"/>
<g:set var="numDomains" value="${grailsApplication.domainClasses.size()}"/>
<g:set var="numPlugins" value="${pluginManager.allPlugins.size()}"/>
<!doctype html>
<html>
<head>
<title>Welcome to Grails + Tailwind</title>
<meta name="layout" content="main"/>
</head>
<body>
<main id="content" role="main" class="space-y-8">
<section class="card">
<h1 class="text-3xl font-semibold tracking-tight">
Welcome to Grails + Tailwind CSS
</h1>
<p class="mt-2 max-w-2xl text-base text-gray-600 dark:text-gray-400">
This page is rendered by a Grails 8 GSP layout and styled entirely with
Tailwind CSS 4 utilities. The dark-mode toggle in the navbar persists
across reloads via <code class="rounded bg-gray-100 px-1 py-0.5 text-sm dark:bg-gray-700">localStorage</code>.
</p>
<div class="mt-6 flex flex-wrap gap-3">
<a href="https://grails.apache.org/guides/grails-tailwindcss/8/guide/index.html"
class="btn-primary"
target="_blank" rel="noopener">
Read the guide
</a>
<a href="https://github.com/grails-guides/grails-tailwindcss/tree/grails8/complete"
class="inline-flex items-center justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 dark:hover:bg-gray-700"
target="_blank" rel="noopener">
View source
</a>
</div>
</section>
<section class="grid grid-cols-1 gap-6 md:grid-cols-3">
<div class="card">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Runtime
</h2>
<dl class="mt-4 space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Grails</dt>
<dd class="font-medium tabular-nums"><g:meta name="info.app.grailsVersion"/></dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Spring Boot</dt>
<dd class="font-medium tabular-nums">${SpringBootVersion.getVersion()}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Spring</dt>
<dd class="font-medium tabular-nums">${SpringVersion.getVersion()}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Groovy</dt>
<dd class="font-medium tabular-nums">${GroovySystem.getVersion()}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">JVM</dt>
<dd class="font-medium tabular-nums">${System.getProperty('java.version')}</dd>
</div>
</dl>
</div>
<div class="card">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Environment
</h2>
<dl class="mt-4 space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">App name</dt>
<dd class="font-medium"><g:meta name="info.app.name"/></dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Mode</dt>
<dd class="font-medium">${Environment.current.name}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Reloading</dt>
<dd class="font-medium">
<g:if test="${Environment.reloadingAgentEnabled}">
<span class="inline-flex items-center gap-1 rounded-full bg-green-100 px-2 py-0.5 text-xs font-medium text-green-800 dark:bg-green-900/40 dark:text-green-300">
Active
</span>
</g:if>
<g:else>
<span class="inline-flex items-center gap-1 rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-700 dark:bg-gray-700 dark:text-gray-300">
Inactive
</span>
</g:else>
</dd>
</div>
</dl>
</div>
<div class="card">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Artefacts
</h2>
<dl class="mt-4 space-y-2 text-sm">
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Controllers</dt>
<dd class="font-medium tabular-nums">${numControllers}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Domains</dt>
<dd class="font-medium tabular-nums">${numDomains}</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-600 dark:text-gray-400">Plugins</dt>
<dd class="font-medium tabular-nums">${numPlugins}</dd>
</div>
</dl>
</div>
</section>
</main>
</body>
</html>[]
Reload http://localhost:8080 after ./gradlew bootRun and you should see the Tailwind-styled cards and the dark-mode toggle in the top right.
Files touched in this chapter:
-
grails-app/views/layouts/main.gsp -
grails-app/views/index.gsp
7 Class-Based Dark Mode
The @custom-variant dark (&:where(.dark, .dark *)); line in input.css made every dark:-prefixed utility opt-in on the presence of the dark class on <html>. Two small bits of markup in main.gsp flip it on and off.
The first is a pre-paint script in <head> that runs before any styled content reaches the screen:
<%-- Pre-paint dark-mode resolution: read the user's saved choice (or
system preference) and apply the `dark` class on <html> BEFORE
the body renders. This avoids the light-to-dark flash on reload. --%>
<script>
(function () {
var saved = localStorage.getItem('theme');
var prefers = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (saved === 'dark' || (!saved && prefers)) {
document.documentElement.classList.add('dark');
}
})();
</script>[]
It reads the user’s saved choice (or the OS preference if the user has not chosen) and adds the dark class to <html> synchronously. Because it runs before the body renders there is no light-to-dark flash on reload.
The second is the toggle button in the navbar plus a click handler at the end of the body that flips the class and writes the new value to localStorage:
<button id="theme-toggle"
type="button"
aria-label="Toggle dark mode"
class="inline-flex h-9 w-9 items-center justify-center rounded-md border border-gray-300 bg-white text-gray-700 shadow-sm hover:bg-gray-50 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600">
<span aria-hidden="true" class="dark:hidden">☼</span>
<span aria-hidden="true" class="hidden dark:inline">☽</span>
</button>[]
<script>
document.getElementById('theme-toggle').addEventListener('click', function () {
var html = document.documentElement;
html.classList.toggle('dark');
localStorage.setItem('theme', html.classList.contains('dark') ? 'dark' : 'light');
});
</script>[]
Notice the icon swap: the sun glyph is hidden in dark mode (dark:hidden), the moon glyph is hidden in light mode (hidden dark:inline). The same dark:/non-dark: pairing pattern works for any element whose appearance depends on theme.
Files touched in this chapter:
-
src/main/css/input.css -
grails-app/views/layouts/main.gsp
8 Reusable Component Classes with @apply
Two patterns repeat across the welcome page: the primary action button and the white card with a header. Once a utility chain shows up four or five times across GSPs it is worth promoting to a named class in the @layer components block.
The relevant section of input.css:
/*
* A small component layer: classes you can spell once and reuse across
* GSPs without the full utility chain. Good fit for buttons, cards, and
* form-control wrappers that the Grails Fields plugin will eventually
* render around your inputs.
*/
@layer components {
.btn-primary {
@apply inline-flex items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-offset-gray-900;
}
.card {
@apply rounded-lg border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-800;
}
.nav-link {
@apply text-sm font-medium text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white;
}
}[]
GSPs then reference them by the short name:
<a href="https://grails.apache.org/guides/grails-tailwindcss/8/guide/index.html" class="btn-primary">Read the guide</a>
<section class="card">...</section>
<a href="https://grails.apache.org/docs/" class="nav-link">Docs</a>
Three things to notice:
-
@applyis still supported in v4, which keeps the component layer readable even for long utility chains. -
Tailwind 4 also introduces
@utilityfor custom utilities. For this guide we stay with@layer componentsbecause the goal is a tiny named component surface that reads naturally in GSPs. -
Component classes do not defeat purging. The scanner still decides what utilities to emit from the classes and variants it can see in your source files.
|
Dynamic class names in Groovy code: if a controller or tag library composes a class string at runtime, Tailwind’s static analyser cannot see the result. In v4 the explicit fix is |
Files touched in this chapter:
-
src/main/css/input.css -
grails-app/views/layouts/main.gsp -
grails-app/views/index.gsp
9 Do you need help with Grails?
Help with Apache Grails
Apache Grails is supported by an active community of contributors and the Apache Software Foundation. If you need help working through a guide, want to discuss the framework, or have run into something that looks like a bug, the channels below are the right place to start.
-
Slack - real-time conversation with the Apache Grails community.
-
Developer mailing list - design discussions and contributor coordination.
-
Users mailing list - end-user questions and answers.
-
Issue tracker on GitHub - file a bug or feature request against the framework.
For Grails plugins, see the matching project on the apache org or the plugin’s own GitHub repository.
Files touched in this chapter:
-
None