You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/03 19:05:55 UTC
(grails-static-website) branch add-grails-tailwindcss-v8 created (now 2b7fe54ed30)
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a change to branch add-grails-tailwindcss-v8
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git
at 2b7fe54ed30 Add grails-tailwindcss v8 guide
This branch includes the following new commits:
new 2b7fe54ed30 Add grails-tailwindcss v8 guide
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(grails-static-website) 01/01: Add grails-tailwindcss v8 guide
Posted by ja...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
jamesfredley pushed a commit to branch add-grails-tailwindcss-v8
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git
commit 2b7fe54ed30ab6c1f47bb9e7bcb9bea7a20eb027
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sun May 3 15:05:40 2026 -0400
Add grails-tailwindcss v8 guide
Worked example for wiring Tailwind CSS 4 into a Grails 8 GSP application
via a Gradle-driven
px @tailwindcss/cli build step. Targets the same
audience as the recently-landed grails-fields-custom-widgets-and-wrappers
v8 guide: server-rendered Grails apps that want a modern utility-first
CSS pipeline without abandoning GSP for an SPA.
Sample-app flavour. Upstream is grails-guides/grails-tailwindcss on the
grails8 branch, with the standard initial/ (vanilla Grails 8 starter
generated from prev-snapshot.grails.org) + complete/ (Tailwind wiring
applied) layout. The complete/ build was verified end-to-end:
`npm install` + `npx @tailwindcss/cli` produces a 16KB minified
`app.css` in 62ms against Tailwind 4.2.4.
The 14 chapters walk through the full integration:
- Getting Started (whatYouWillBuild, requirements, howto)
- Creating the Application (download via the forge create endpoint)
- Installing Tailwind CSS 4
- Configuring Tailwind for Grails (CSS-first config via @source +
@custom-variant; no tailwind.config.js needed in v4)
- Wiring Tailwind into the Gradle Build (npmInstall + tailwindBuild
Exec tasks hooked into processResources and compileGroovy)
- Applying Tailwind to the GSP Layout (rewritten main.gsp + index.gsp)
- Class-Based Dark Mode (pre-paint script + localStorage toggle)
- Reusable Component Classes with @apply (btn-primary, card, nav-link)
Verified locally:
./gradlew validateGuides -PvalidationMode=both
[validateGuides] mode=both: 86 guide(s) parsed, 0 SKIP-warned, 0 errors
./gradlew renderGuide_grails_tailwindcss_8 --rerun-tasks --no-configuration-cache
Renders all 14 chapter HTML pages, no Unresolved directive errors,
clean prev/next navigation. (--no-configuration-cache mirrors the
existing publishMainSite workaround in 0a0201f898.)
Assisted-by: claude-code:claude-opus-4-7
---
conf/guides.yml | 45 ++++++
.../v8/guide/componentLayer.adoc | 38 +++++
.../v8/guide/configureTailwind.adoc | 9 ++
guides/grails-tailwindcss/v8/guide/createApp.adoc | 1 +
guides/grails-tailwindcss/v8/guide/darkMode.adoc | 39 ++++++
guides/grails-tailwindcss/v8/guide/download.adoc | 16 +++
guides/grails-tailwindcss/v8/guide/firstPaint.adoc | 23 +++
.../v8/guide/gettingStarted.adoc | 5 +
.../grails-tailwindcss/v8/guide/gradleWiring.adoc | 19 +++
.../v8/guide/helpWithGrails.adoc | 1 +
guides/grails-tailwindcss/v8/guide/howto.adoc | 15 ++
guides/grails-tailwindcss/v8/guide/inputCss.adoc | 15 ++
.../v8/guide/installTailwind.adoc | 16 +++
.../grails-tailwindcss/v8/guide/requirements.adoc | 6 +
.../v8/guide/whatYouWillBuild.adoc | 8 ++
guides/grails-tailwindcss/v8/snippets/build.gradle | 156 +++++++++++++++++++++
.../grails-app/assets/stylesheets/application.css | 9 ++
.../v8/snippets/grails-app/views/index.gsp | 123 ++++++++++++++++
.../v8/snippets/grails-app/views/layouts/main.gsp | 72 ++++++++++
guides/grails-tailwindcss/v8/snippets/package.json | 14 ++
.../v8/snippets/src/main/css/input.css | 46 ++++++
21 files changed, 676 insertions(+)
diff --git a/conf/guides.yml b/conf/guides.yml
index 9a0bcf68f2c..f3831e58cb1 100644
--- a/conf/guides.yml
+++ b/conf/guides.yml
@@ -3705,6 +3705,51 @@ guides:
helpWithGrails:
title: Do you need help with Grails?
+ - name: 'grails-tailwindcss'
+ title: 'Tailwind CSS 4 with Grails 8'
+ subtitle: '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'
+ category: 'Web Layer'
+ publicationDate: '2026-05-03'
+ versions:
+ '8':
+ sourcePath: guides/grails-tailwindcss/v8
+ publicationDate: '2026-05-03'
+ tags:
+ - 'tailwindcss'
+ - 'css'
+ - 'gsp'
+ - 'asset-pipeline'
+ - 'grails8'
+ sampleRef:
+ repo: 'grails-guides/grails-tailwindcss'
+ branch: 'grails8'
+ toc:
+ gettingStarted:
+ title: Getting Started
+ whatYouWillBuild: What You Will Build
+ requirements: What You Will Need
+ howto: How to Complete the Guide
+ createApp:
+ title: Creating the Application
+ download: Download a Grails 8 Snapshot Starter
+ installTailwind:
+ title: Installing Tailwind CSS 4
+ configureTailwind:
+ title: Configuring Tailwind for Grails
+ inputCss: The input.css Entry Point
+ gradleWiring:
+ title: Wiring Tailwind into the Gradle Build
+ firstPaint:
+ title: Applying Tailwind to the GSP Layout
+ darkMode:
+ title: Class-Based Dark Mode
+ componentLayer:
+ title: Reusable Component Classes with @apply
+ helpWithGrails:
+ title: Do you need help with Grails?
+
- name: 'grails-taglib-wyswyg-trix'
title: 'Create and test a Grails 5 TagLib; integrate the Trix WYSWYG editor'
subtitle: 'Learn how to integrate Trix (the rich text editor created by Basecamp) with your Grails app with the help of a custom TagLib'
diff --git a/guides/grails-tailwindcss/v8/guide/componentLayer.adoc b/guides/grails-tailwindcss/v8/guide/componentLayer.adoc
new file mode 100644
index 00000000000..2a944f73730
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/componentLayer.adoc
@@ -0,0 +1,38 @@
+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`:
+
+[source,css]
+----
+@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:
+
+[source,html]
+----
+<a href="${createLink(controller: 'book', action: 'create')}" class="btn-primary">New book</a>
+<div class="card">...</div>
+<a href="..." class="nav-link">Docs</a>
+----
+
+Three things to notice:
+
+* `@apply` is still allowed in v4 - the deprecation warnings you may have seen in early v4 betas were about scoping rules, not the directive itself.
+* Component classes in v4 still respect the source order of `@layer base`, `@layer components`, `@layer utilities`. A utility class on a single element will override the same property in a `@layer components` rule.
+* Component classes do *not* defeat purging: the scanner picks them up via the `@source` directives just like raw utility classes.
+
+[TIP]
+====
+*Dynamic class names in Groovy code:* if a controller or tag library composes a class string at runtime (e.g. `"alert-${level}"`), Tailwind's static analyser cannot see the result and will purge those classes from the output. Add the variants to a safelist by writing them out as a comment in any scanned file (`/* alert-info alert-warn alert-error */`) - the scanner reads literal class names from any file matched by `@source`.
+====
diff --git a/guides/grails-tailwindcss/v8/guide/configureTailwind.adoc b/guides/grails-tailwindcss/v8/guide/configureTailwind.adoc
new file mode 100644
index 00000000000..97be867ce56
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/configureTailwind.adoc
@@ -0,0 +1,9 @@
+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 JIT scanner does not know about `.gsp` files out of the box, and it is conservative about reading utility-class string literals from `.groovy` source. Both have to be added with explicit `@source` directives.
+* *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 so `dark:` utilities apply when an ancestor has the `dark` class.
+* *Reusable component classes (optional).* Anything you find yourself spelling more than three or four times across GSPs is a candidate for the `@layer components` block.
+
+All three live in `src/main/css/input.css`, which we will write next.
diff --git a/guides/grails-tailwindcss/v8/guide/createApp.adoc b/guides/grails-tailwindcss/v8/guide/createApp.adoc
new file mode 100644
index 00000000000..98b8ae21354
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/createApp.adoc
@@ -0,0 +1 @@
+Generate a fresh Apache Grails 8 web application from link:https://prev-snapshot.grails.org[prev-snapshot.grails.org]. This is the same forge that produced the `initial/` tree in the upstream repository.
diff --git a/guides/grails-tailwindcss/v8/guide/darkMode.adoc b/guides/grails-tailwindcss/v8/guide/darkMode.adoc
new file mode 100644
index 00000000000..f9d176d24fe
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/darkMode.adoc
@@ -0,0 +1,39 @@
+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 four-line pre-paint script in `<head>` that runs before any styled content reaches the screen:
+
+[source,html]
+----
+<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`:
+
+[source,html]
+----
+<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.
diff --git a/guides/grails-tailwindcss/v8/guide/download.adoc b/guides/grails-tailwindcss/v8/guide/download.adoc
new file mode 100644
index 00000000000..2c28be25802
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/download.adoc
@@ -0,0 +1,16 @@
+The forge exposes a simple HTTP create endpoint that returns a zip:
+
+[source,bash]
+----
+curl -L -o tailwind-app.zip \
+ "https://prev-snapshot.grails.org/create/web/example.tailwind?lang=GROOVY&build=GRADLE&test=SPOCK&javaVersion=JDK_21"
+unzip tailwind-app.zip
+cd tailwind
+----
+
+The zip unpacks into a `tailwind/` directory containing the standard Grails 8 web layout (`grails-app/`, `src/`, `gradle/`, `build.gradle`, `gradle.properties`).
+
+[NOTE]
+====
+The forge's default `build.gradle` ships with `compileJava.options.release = 17` even when `javaVersion=JDK_21` is requested. The `complete/` tree in the upstream repo already patches this to `21`; do the same in your local checkout if you want JDK 21 source-level features.
+====
diff --git a/guides/grails-tailwindcss/v8/guide/firstPaint.adoc b/guides/grails-tailwindcss/v8/guide/firstPaint.adoc
new file mode 100644
index 00000000000..5ed6115a732
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/firstPaint.adoc
@@ -0,0 +1,23 @@
+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:
+
+[source,html]
+.grails-app/views/layouts/main.gsp
+----
+include::../snippets/grails-app/views/layouts/main.gsp[]
+----
+
+A few patterns are worth pointing at:
+
+* The outer `<body>` carries `bg-gray-50 dark:bg-gray-900` so the colour scheme flips at the top level.
+* The navbar uses `mx-auto max-w-7xl` plus a flex row, replacing the Bootstrap `navbar` component with a hand-rolled equivalent that is easier to customise.
+* The footer link is the `text-blue-600 hover:underline dark:text-blue-400` triplet that you will repeat all over the app once you commit to the utility-first approach.
+
+The welcome page itself follows the same recipe:
+
+[source,html]
+.grails-app/views/index.gsp
+----
+include::../snippets/grails-app/views/index.gsp[]
+----
+
+Reload `http://localhost:8080` after `./gradlew bootRun` and you should see the Tailwind-styled cards and the dark-mode toggle in the top right.
diff --git a/guides/grails-tailwindcss/v8/guide/gettingStarted.adoc b/guides/grails-tailwindcss/v8/guide/gettingStarted.adoc
new file mode 100644
index 00000000000..be8ba3498cc
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/gettingStarted.adoc
@@ -0,0 +1,5 @@
+In this guide you will wire link:https://tailwindcss.com[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 pattern works for any server-rendered Grails app: you keep GSPs and the asset pipeline, swap Bootstrap for Tailwind utilities, and let a Gradle-driven `npx @tailwindcss/cli` step regenerate the CSS on every build.
+
+This guide targets Apache Grails 8.
diff --git a/guides/grails-tailwindcss/v8/guide/gradleWiring.adoc b/guides/grails-tailwindcss/v8/guide/gradleWiring.adoc
new file mode 100644
index 00000000000..0e761f2a3a3
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/gradleWiring.adoc
@@ -0,0 +1,19 @@
+A two-task block at the bottom of `build.gradle` is everything the build needs:
+
+[source,groovy]
+.build.gradle
+----
+include::../snippets/build.gradle[tag=tailwind,indent=0]
+----
+
+Two new `Exec` tasks are added:
+
+* `npmInstall` runs `npm install` and caches its result. The `outputs.dir('node_modules')` declaration plus the `inputs.file('package.json')` declaration mean Gradle reruns it only when `package.json` changes.
+* `tailwindBuild` calls `npx @tailwindcss/cli` with the same `-i` and `-o` paths used by the npm scripts. Its `inputs` cover `package.json`, the entry CSS, and the two directory trees we declared as `@source` paths in `input.css`. Its single output is the compiled `app.css`.
+
+The two `tasks.named` lines hook `tailwindBuild` into the standard Grails build graph so plain `./gradlew bootRun` (or `assemble`, or `bootJar`) regenerates `app.css` on every cold start. For iterative work without Gradle in the loop, run `npm run watch` in a second shell.
+
+[NOTE]
+====
+Asset-pipeline manifest: replace the contents of `grails-app/assets/stylesheets/application.css` with `*= require app` so the manifest pulls in the Tailwind output instead of the Bootstrap CSS the starter shipped with. The `app.css` file itself is regenerated on every build, so it is git-ignored.
+====
diff --git a/guides/grails-tailwindcss/v8/guide/helpWithGrails.adoc b/guides/grails-tailwindcss/v8/guide/helpWithGrails.adoc
new file mode 100644
index 00000000000..e062f614b1a
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/helpWithGrails.adoc
@@ -0,0 +1 @@
+include::{commondir}/common-helpWithGrails.adoc[]
diff --git a/guides/grails-tailwindcss/v8/guide/howto.adoc b/guides/grails-tailwindcss/v8/guide/howto.adoc
new file mode 100644
index 00000000000..bd5f010a454
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/howto.adoc
@@ -0,0 +1,15 @@
+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:
+
+[source,bash]
+----
+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 snapshot project, generated from `https://prev-snapshot.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.
diff --git a/guides/grails-tailwindcss/v8/guide/inputCss.adoc b/guides/grails-tailwindcss/v8/guide/inputCss.adoc
new file mode 100644
index 00000000000..dd98cf6d6bd
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/inputCss.adoc
@@ -0,0 +1,15 @@
+Create the Tailwind entry point at `src/main/css/input.css`:
+
+[source,css]
+.src/main/css/input.css
+----
+include::../snippets/src/main/css/input.css[]
+----
+
+Three 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 `@source` directives tell the scanner exactly where to look. The starter the forge produces puts views under `grails-app/views`, controllers under `grails-app/controllers`, services under `grails-app/services`, and any helper code (tag libs, command objects, traits) under `src/main/groovy`. Adding a new directory tree later means another `@source` line; nothing else.
+* The `@custom-variant dark (&:where(.dark, .dark *));` line redefines `dark:` so it applies whenever an ancestor (typically `<html>`) carries the `dark` class. Without this override the v4 default uses `prefers-color-scheme`, which honours the OS theme but does not let the user override it.
+
+The `@layer components` block at the bottom defines `.btn-primary`, `.card`, and `.nav-link` - these are referenced from `main.gsp` and `index.gsp` in later chapters.
diff --git a/guides/grails-tailwindcss/v8/guide/installTailwind.adoc b/guides/grails-tailwindcss/v8/guide/installTailwind.adoc
new file mode 100644
index 00000000000..d68edbf6a9d
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/installTailwind.adoc
@@ -0,0 +1,16 @@
+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:
+
+[source,json]
+.package.json
+----
+include::../snippets/package.json[]
+----
+
+Then run `npm install` once to populate `node_modules/`:
+
+[source,bash]
+----
+npm install
+----
+
+The `tailwindBuild` Gradle task we will add in a later chapter calls `npm install` automatically the first time it runs (and skips it on subsequent invocations as long as `package.json` has not changed), so committing the lock file is enough; no hand-rolled bootstrap step is needed in CI.
diff --git a/guides/grails-tailwindcss/v8/guide/requirements.adoc b/guides/grails-tailwindcss/v8/guide/requirements.adoc
new file mode 100644
index 00000000000..75784f0caba
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/requirements.adoc
@@ -0,0 +1,6 @@
+To complete this guide you will need:
+
+* JDK 21
+* link:https://nodejs.org[Node.js] 20+ and a recent npm (the Tailwind 4 CLI is published as `@tailwindcss/cli` on npm; no other build tooling is required)
+* A Bash, PowerShell, or `cmd` shell capable of running the included Gradle wrapper (`./gradlew` or `gradlew.bat`)
+* About 30 minutes
diff --git a/guides/grails-tailwindcss/v8/guide/whatYouWillBuild.adoc b/guides/grails-tailwindcss/v8/guide/whatYouWillBuild.adoc
new file mode 100644
index 00000000000..df26b190805
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/guide/whatYouWillBuild.adoc
@@ -0,0 +1,8 @@
+By the end of the guide your application will:
+
+* Render `grails-app/views/layouts/main.gsp` and `grails-app/views/index.gsp` with 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) into `grails-app/assets/stylesheets/app.css` via a `tailwindBuild` Gradle task that runs before `processResources` and `compileGroovy`.
+* Toggle dark mode at runtime by adding the `dark` class on `<html>`, persisting the choice to `localStorage` so reloads do not flash light-to-dark.
+* Expose two reusable component classes (`btn-primary`, `card`) defined in the `@layer components` block of `input.css` and 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.
diff --git a/guides/grails-tailwindcss/v8/snippets/build.gradle b/guides/grails-tailwindcss/v8/snippets/build.gradle
new file mode 100644
index 00000000000..0955ea3558b
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/build.gradle
@@ -0,0 +1,156 @@
+buildscript {
+ repositories {
+ mavenCentral()
+ maven {
+ url = 'https://repo.grails.org/grails/restricted'
+ }
+ maven {
+ url = 'https://repository.apache.org/content/groups/snapshots'
+ content {
+ includeVersionByRegex('org[.]apache[.]grails.*', '.*', '.*-SNAPSHOT')
+ }
+ content {
+ includeVersionByRegex('org[.]apache[.]groovy.*', 'groovy.*', '.*-SNAPSHOT')
+ }
+ mavenContent {
+ snapshotsOnly()
+ }
+ }
+ maven {
+ url = 'https://repository.apache.org/content/groups/staging'
+ content {
+ includeVersionByRegex('org[.]apache[.]grails[.]gradle', 'grails-publish', '.*')
+ }
+ content {
+ includeVersionByRegex('org[.]apache[.]groovy.*', 'groovy.*', '.*')
+ }
+ mavenContent {
+ releasesOnly()
+ }
+ }
+ }
+ dependencies { // Not Published to Gradle Plugin Portal
+ classpath "cloud.wondrify:asset-pipeline-gradle"
+ classpath platform("org.apache.grails:grails-bom:$grailsVersion")
+ classpath "org.apache.grails:grails-data-hibernate5"
+ classpath "org.apache.grails:grails-gradle-plugins"
+ }
+}
+
+plugins {
+ id "war"
+ id "idea"
+ id "eclipse"
+}
+
+// Not Published to Gradle Plugin Portal
+apply plugin: "org.apache.grails.gradle.grails-web"
+apply plugin: "org.apache.grails.gradle.grails-gsp"
+apply plugin: "cloud.wondrify.asset-pipeline"
+
+group = "example"
+
+repositories {
+ mavenCentral()
+ maven {
+ url = 'https://repo.grails.org/grails/restricted'
+ }
+ maven {
+ url = 'https://repository.apache.org/content/groups/snapshots'
+ content {
+ includeVersionByRegex('org[.]apache[.]grails.*', '.*', '.*-SNAPSHOT')
+ }
+ content {
+ includeVersionByRegex('org[.]apache[.]groovy.*', 'groovy.*', '.*-SNAPSHOT')
+ }
+ mavenContent {
+ snapshotsOnly()
+ }
+ }
+ maven {
+ url = 'https://repository.apache.org/content/groups/staging'
+ content {
+ includeVersionByRegex('org[.]apache[.]grails[.]gradle', 'grails-publish', '.*')
+ }
+ content {
+ includeVersionByRegex('org[.]apache[.]groovy.*', 'groovy.*', '.*')
+ }
+ mavenContent {
+ releasesOnly()
+ }
+ }
+}
+
+dependencies {
+ profile "org.apache.grails.profiles:web"
+ developmentOnly "org.springframework.boot:spring-boot-devtools" // Spring Boot DevTools may cause performance slowdowns or compatibility issues on larger applications
+ implementation platform("org.apache.grails:grails-bom:$grailsVersion")
+ implementation "org.apache.grails:grails-core"
+ implementation "org.apache.grails:grails-data-hibernate5"
+ implementation "org.apache.grails:grails-databinding"
+ implementation "org.apache.grails:grails-events"
+ implementation "org.apache.grails:grails-gsp"
+ implementation "org.apache.grails:grails-interceptors"
+ implementation "org.apache.grails:grails-layout"
+ implementation "org.apache.grails:grails-logging"
+ implementation "org.apache.grails:grails-rest-transforms"
+ implementation "org.apache.grails:grails-scaffolding"
+ implementation "org.apache.grails:grails-services"
+ implementation "org.apache.grails:grails-url-mappings"
+ implementation "org.apache.grails:grails-web-boot"
+ implementation "org.springframework.boot:spring-boot-autoconfigure"
+ implementation "org.springframework.boot:spring-boot-starter"
+ implementation "org.springframework.boot:spring-boot-starter-actuator"
+ implementation "org.springframework.boot:spring-boot-starter-logging"
+ implementation "org.springframework.boot:spring-boot-starter-tomcat"
+ implementation "org.springframework.boot:spring-boot-starter-validation"
+ implementation "org.springframework.boot:spring-boot-tomcat"
+ console "org.apache.grails:grails-console"
+ runtimeOnly "cloud.wondrify:asset-pipeline-grails"
+ runtimeOnly "com.h2database:h2"
+ runtimeOnly "com.zaxxer:HikariCP"
+ runtimeOnly "org.fusesource.jansi:jansi"
+ integrationTestImplementation testFixtures("org.apache.grails:grails-geb")
+ testImplementation "org.apache.grails:grails-testing-support-datamapping"
+ testImplementation "org.apache.grails:grails-testing-support-web"
+ testImplementation "org.mockito:mockito-core"
+ testImplementation "org.spockframework:spock-core"
+}
+
+compileJava.options.release = 21
+
+tasks.withType(Test).configureEach {
+ useJUnitPlatform()
+}
+
+// tag::tailwind[]
+def npxCmd = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'npx.cmd' : 'npx'
+def npmCmd = org.gradle.internal.os.OperatingSystem.current().isWindows() ? 'npm.cmd' : 'npm'
+
+tasks.register('npmInstall', Exec) {
+ description = 'Install npm dependencies (Tailwind CSS 4 CLI)'
+ group = 'tailwind'
+ inputs.file('package.json')
+ outputs.dir('node_modules')
+ commandLine npmCmd, 'install', '--no-fund', '--no-audit'
+}
+
+tasks.register('tailwindBuild', Exec) {
+ description = 'Compile src/main/css/input.css into grails-app/assets/stylesheets/app.css via the Tailwind 4 CLI'
+ group = 'tailwind'
+ dependsOn 'npmInstall'
+ inputs.file('package.json')
+ inputs.file('src/main/css/input.css')
+ inputs.dir('grails-app/views')
+ inputs.dir('grails-app/controllers')
+ 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' }
+// end::tailwind[]
+
diff --git a/guides/grails-tailwindcss/v8/snippets/grails-app/assets/stylesheets/application.css b/guides/grails-tailwindcss/v8/snippets/grails-app/assets/stylesheets/application.css
new file mode 100644
index 00000000000..62715727608
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/grails-app/assets/stylesheets/application.css
@@ -0,0 +1,9 @@
+/*
+ * Asset-pipeline manifest. The `app.css` file is the Tailwind 4 CLI
+ * output (regenerated by the `tailwindBuild` Gradle task on every build);
+ * it is git-ignored. `require_self` keeps any rules added below this
+ * comment block at the bottom of the bundle.
+ *
+ *= require app
+ *= require_self
+ */
diff --git a/guides/grails-tailwindcss/v8/snippets/grails-app/views/index.gsp b/guides/grails-tailwindcss/v8/snippets/grails-app/views/index.gsp
new file mode 100644
index 00000000000..a7bcc27834b
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/grails-app/views/index.gsp
@@ -0,0 +1,123 @@
+<%@ page import="grails.util.Environment"%>
+<%@ page import="org.springframework.boot.SpringBootVersion"%>
+<%@ page import="org.springframework.core.SpringVersion"%>
+<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"
+ 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>
diff --git a/guides/grails-tailwindcss/v8/snippets/grails-app/views/layouts/main.gsp b/guides/grails-tailwindcss/v8/snippets/grails-app/views/layouts/main.gsp
new file mode 100644
index 00000000000..cd779aa1c04
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/grails-app/views/layouts/main.gsp
@@ -0,0 +1,72 @@
+<!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:flashMessages />
+ <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>
+</body>
+</html>
diff --git a/guides/grails-tailwindcss/v8/snippets/package.json b/guides/grails-tailwindcss/v8/snippets/package.json
new file mode 100644
index 00000000000..63048466d65
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "grails-tailwindcss",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Tailwind CSS 4 build wiring for the grails-tailwindcss/v8 sample app.",
+ "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"
+ }
+}
diff --git a/guides/grails-tailwindcss/v8/snippets/src/main/css/input.css b/guides/grails-tailwindcss/v8/snippets/src/main/css/input.css
new file mode 100644
index 00000000000..ad4edaa2f72
--- /dev/null
+++ b/guides/grails-tailwindcss/v8/snippets/src/main/css/input.css
@@ -0,0 +1,46 @@
+/*
+ * 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 "../../../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;
+ }
+}