Building Grails Custom Tag Libraries for Reusable Views
Grails custom tag libraries give teams a practical way to move repeated presentation logic out of GSP pages and into reusable components. A tag can render a status badge, format a date, build a secure link, display validation feedback, or wrap a small piece of HTML that appears across many screens.
This approach is particularly valuable in applications maintained by several developers. Instead of copying Groovy expressions between templates, a team can define a consistent interface with named attributes and a body closure. Designers and front-end developers can then use a concise GSP tag without needing to understand the service or domain logic behind it.
The examples below use common Grails and Groovy conventions, with attention to output safety, testing, accessibility, and deployment. They also suit applications built for Australian organisations, whether the product serves customers in Sydney, internal users in Melbourne, or a distributed team working across Brisbane, Perth and Adelaide.
Understanding The Tag Library Model
A Grails tag library is a Groovy class stored in the application's grails-app/taglib directory. Grails detects the class and makes its tag closures available to GSP views. A simple class might look like this:
package example
class UiTagLib {
static namespace = 'ui'
def status = { attrs, body ->
def label = attrs.label ?: 'Unknown'
def cssClass = attrs.className ?: 'muted'
out << "<span class=\"status ${cssClass}\">"
out << "${label.encodeAsHTML()}"
out << "</span>"
}
}
The namespace determines how the tag is called. With the example above, a GSP page can use:
<ui:status label="Active" className="positive" />
The attrs map contains attributes supplied by the view, while body is a closure representing content nested inside the tag. The out writer sends generated output into the response. For a tag with nested content, call body() and encode or structure the result according to the intended output.
def panel = { attrs, body ->
def heading = attrs.heading ?: ''
out << '<section class="panel">'
if (heading) {
out << "<h2>${heading.encodeAsHTML()}</h2>"
}
out << body()
out << '</section>'
}
Grails versions differ in their preferred APIs and default encoding behaviour, so check the documentation for the version used by the application. Older projects may rely on encodeAsHTML(), while newer applications can use codecs and explicit output handling. Keeping that decision consistent across the codebase prevents confusing differences between local development and production.
Designing Tags With Clear Boundaries
A custom tag should solve a view concern rather than become a hidden controller or service. Formatting a currency amount, displaying a validation message, or choosing a CSS class belongs naturally in a tag library. Fetching a large set of records, applying business policy, or changing persistent state usually belongs in a service.
A useful tag API has a small number of predictable attributes. For example, a currency tag might accept amount, currency, and showZero, rather than exposing a domain object with many optional behaviours:
import java.text.NumberFormat
import java.util.Locale
class MoneyTagLib {
static namespace = 'money'
def format = { attrs ->
BigDecimal amount = attrs.amount as BigDecimal
String currency = attrs.currency ?: 'AUD'
Locale locale = attrs.locale ?: Locale.ENGLISH
if (amount == null) {
out << ''
return
}
def formatter = NumberFormat.getCurrencyInstance(new Locale('en', 'AU'))
formatter.currency = Currency.getInstance(currency)
out << formatter.format(amount).encodeAsHTML()
}
}
A tag intended for Australian commerce may need to display Australian dollars, GST-inclusive prices, or local date conventions. Keep those rules explicit. A financial tag should not silently change a value because the server moved from Sydney to a data centre in Singapore. Pass locale and currency deliberately, and document whether the input is gross, net, tax-inclusive, or tax-exclusive.
Namespacing is another important design choice. Common namespaces such as ui, format, security, and app make GSP calls easy to recognise. Avoid generic names that conflict with built-in Grails tags. If a project has separate administrative and customer-facing interfaces, namespaces such as admin and shop can make ownership clearer without duplicating implementation.
Making Output Safe And Accessible
Reusable view code must treat user-controlled values as untrusted. A tag that prints a customer name, search term, address, or error message should encode it for HTML. Concatenating raw request data into markup can create a cross-site scripting vulnerability. This matters for public-facing Australian services handling customer registrations, bookings, or payments.
Attributes that represent CSS classes, element names, or URLs require additional care. HTML encoding alone does not validate a class name or confirm that a URL is safe. Restrict class values to a known set, use approved URL builders, and avoid accepting raw markup unless the tag's contract explicitly requires trusted content.
A link tag demonstrates the difference between presentation and safety:
class LinkTagLib {
static namespace = 'app'
def actionLink = { attrs, body ->
def controller = attrs.controller
def action = attrs.action ?: 'index'
def id = attrs.id
def text = attrs.text ?: body()
if (!controller || !text) {
return
}
def target = createLink(
controller: controller,
action: action,
id: id
)
out << "<a href=\"${target.encodeAsHTML()}\">"
out << text.toString().encodeAsHTML()
out << '</a>'
}
}
Accessibility should be part of the tag contract rather than a later patch. A status tag should communicate meaning through text, not colour alone. A modal wrapper should support headings and focus management. A pagination tag should provide useful link text and suitable aria attributes. Teams working on government, health, education, or large retail platforms in Australia may also need to align with WCAG requirements and procurement accessibility standards.
When a tag returns markup from a nested body, make the trust boundary obvious. Body content written by the application may be safe, while content originating from a user profile is not automatically safe. In newer Grails applications, use the framework's recommended codecs and avoid disabling automatic encoding for convenience.
Testing And Using Tags In GSP Views
A tag library is easier to maintain when its behaviour is tested independently of a full browser workflow. Grails testing support can render a tag and assert on the generated HTML, text, attributes, and empty-value behaviour. Test both the ordinary path and input that could expose unsafe output.
A useful test set for a status tag includes an active value, a missing label, an unexpected CSS class, and a label containing characters such as <, > and &. For a currency tag, test Australian dollars, zero values, negative values, null input, and rounding rules. Dates should be tested around daylight-saving changes because Sydney and Melbourne do not have the same seasonal clock settings as Brisbane, while Perth follows a different time zone altogether.
In a GSP view, keep the call expressive:
<ui:panel heading="${message(code: 'order.summary')}">
<ui:status label="${order.status}" className="${order.statusCssClass}" />
<money:format amount="${order.total}" currency="AUD" />
</ui:panel>
The view should provide data and structure, while the tag controls repeated markup. If the tag starts accepting a dozen attributes, split it into smaller tags or introduce a view model. An overly flexible tag becomes difficult to document and may hide layout decisions in Groovy code.
Reusable tags also work well with internationalisation. Use message codes for labels, button text, validation feedback, and accessible descriptions. An Australian default can be supplied through message bundles, but the tag should not assume that every user is in Australia. This is especially important when a Melbourne-based business expands into New Zealand or supports customers travelling across time zones.
For complex components, document example calls beside the tag class or in the project's developer guide. Include required attributes, defaults, expected output, and whether body content is escaped. A short usage contract helps new team members avoid reverse-engineering a closure from a large GSP file.
Packaging And Deploying Shared View Components
When several Grails applications use the same presentation patterns, consider placing stable tag libraries in a plugin. A plugin can contain tag classes, GSP templates, resources, tests, and documentation. Version it independently so an internal customer portal and a public booking application can adopt changes at controlled times.
Start with application-local tags while the interface is still changing. Extract them only after the attribute names and generated markup have stabilised. Prematurely publishing a plugin can turn a small change into a compatibility problem across multiple products. Semantic versioning, release notes, and deprecation periods are worthwhile when a tag is shared by teams.
A deployment package should include the compiled application and plugin dependencies, but production should not depend on a developer's local Grails installation. Review the build configuration, lock compatible Grails and Groovy versions, and run tests in an environment close to production. Teams supporting systems in Sydney may deploy to a local region for latency or data governance, while a Perth office may require careful planning around maintenance windows and time zones.
Operational documentation can sit alongside general Linux deployment guidance when the application runs on Linux servers or containers. Record required Java versions, environment variables, asset compilation steps, logging locations, and rollback procedures. A custom tag may look like a small view feature, yet a missing plugin dependency or incompatible codec can prevent the entire application from starting.
Tags that display audit information should also have a defined data source. If a screen shows who changed an order or when a customer record was updated, keep audit retrieval in a service and let the tag format the result. Grails applications using Hibernate can benefit from an audit history guide when building a reliable change-tracking feature. The tag should receive prepared data instead of opening database sessions during rendering.
Finally, inspect generated HTML in a browser and test representative pages after deployment. Check responsive layouts at mobile widths, keyboard navigation, screen-reader labels, and Australian date and currency formatting. A reusable component earns its place when it produces consistent, secure output across the application without making each view harder to understand.
Start with one repeated GSP fragment, define a narrow tag interface, and add tests before expanding its responsibilities. Once the component is safe, accessible, and well documented, reuse it across the application or package it for other Grails projects.