Golang is a powerful language for the web. Templ is a language that is used for creating HTML components in Go. We can use Tailwind to style the HTML components.
You can learn more about setting up HTMX, Templ, and Go on our previous blog post.
If you've worked with frontend before, you must be familiar with concepts like components, props, and wrapper components. Building frontends in Go can feel unfamiliar for developers coming from React or Vue. We will learn how to adopt familiar patterns when using Templ with Tailwind to improve the developer experience while keeping compile-time safety and Go's simplicity.
We will start with contrived examples and examine the limitations, as well as how each pattern enhances the developer experience of using the components.
1. Using Children with Template Composition
Let's take the example of a button component:
templ Button(content string) {
<button class="px-4 py-2 bg-black text-white">{ content }</button>
}
We can use the button like:
@Button("Show API Errors")
The limitation of passing content as an argument is that we lose the ability to style the content inside the button. For example, if we wanted to make "API" bold with a <strong> tag, or insert an icon before the text.
We can modify the Button component to accept children:
templ Button() {
<button class="px-4 py-2 bg-black text-white">{ children... }</button>
}
And use the component like:
// Button with Styled Text @Button() { Show <strong>API</strong> Errors } // Button with Icon @Button() {
<span class="inline-flex gap-2"> @CodeIcon() Show API Errors </span>
}
2. Using Props
Now that the contents are much more customizable and extensible.
We can examine another common pattern with frontend components, utilizing props to pass values to a component.
A component can accept properties like: tailwind variants, size, href, disabled, etc. We could pass these values as function arguments:
templ Button(variant string, class string) {
switch variant {
case "bg-indigo":
<button class={ "px-4 py-2 bg-indigo-700 text-white" + class }>
{ children... }
</button>
default:
<button class={ "px-4 py-2 bg-black text-white" + class}>
{ children... }
</button>
}
}
And we'd use the Templ component like:
@Button("", "border border-lime-500")
This pattern has a few drawbacks:
- We need to explicitly specify every argument
- It is not clear what an argument does without looking at the definition of the component.
- In a scenario where a component has a lot of arguments, the component usage can be confusing to look at, for example:
@Button("", "", false, "border border-lime-500", "")
To improve the developer experience, we can create a Props struct with all the arguments and pass it to the component:
type ButtonProps struct {
Variant string
Class string
}
templ Button(props ButtonProps) {
switch props.Variant {
case "bg-indigo":
<button class={ "px-4 py-2 bg-indigo-700 text-white" + props.Class }>
{ children... }
</button>
default:
<button class={ "px-4 py-2 bg-black text-white" + props.Class}>
{ children... }
</button>
}
}
We don't need to specify every value now and can rely on the default values of the struct as well:
@Button(ButtonProps{
Class: "border border-lime-500"
})
We also have the flexibility to add more properties to our component without changing the usage of the components.
3. Custom Variant Types
In the example for the 2nd pattern, we specified the variant as a string. The approach has a few problems. Firstly, we need to always refer to the component definition to understand the different variants, and secondly, we must ensure that we avoid any typos.
Both of these issues can be solved by using a custom Go type for the variant.
We will define a type called ButtonVariant with an underlying type of int.
type ButtonVariant int
We can now start defining variants with the custom types:
const (
Default ButtonVariant = iota
BgIndigo
BgAmber
)
And now we can use the custom variant type in our Props:
type ButtonProps struct {
Variant ButtonVariant
Class string
}
templ Button(props ButtonProps) {
switch props.Variant {
case BgIndigo:
<button class={ "px-4 py-2 bg-indigo-700 text-white" + props.Class }>
{ children... }
</button>
default:
<button class={ "px-4 py-2 bg-black text-white" + props.Class}>
{ children... }
</button>
}
}
Let's use the custom variant type:
@Button(ButtonProps{
Variant: BgIndigo
})
The compiler helps us; if we make a mistake now, it will tell us we are using an undefined type or the wrong type for the Variant. Our IDE will also help us by providing an autocomplete for matching types in the package, so we don't need to look at the component definition or worry about typos.
4. Avoiding Tailwind style conflicts
In the previous examples, we have concatenated the classes passed to the props. A major drawback of this approach is that it can result in multiple classes that apply the same styling.
For example, if we wanted to specify a different padding for the button component, we might try:
@Button(ButtonProps{
Class: "px-6"
})
The resulting class string for the component will look like:
<button class="px-4 py-2 bg-indigo-700 text-white px-6" />
Notice how we ended up with two padding values: px-4 and px-6. Because of how CSS cascade works, the style of px-6 will be ignored.
To avoid style conflicts and customize the existing classes in a Templ component, we can use a package like tailwind-merge-go.
To use it, we simply import the package and use the Merge method:
// ...
import twmerge "github.com/Oudwins/tailwind-merge-go"
type ButtonProps struct {
Class string
}
templ Button(props ButtonProps) {
<button class={ twmerge.Merge("px-4 py-2 bg-indigo-700 text-white", props.Class) }>
{ children... }
</button>
}
}
And now, if we specify a class of px-6 when using the component, it will override px-4.
5. Using packages for scoping components
When building a web frontend, we may end up with multiple components, each with its own Props, Variants, and nested components.
Grouping the components that are used together in a single package can help avoid confusion both when using the component and when working on the components.
Let's take an example of a card component, we might make a package called "card" in src/components/card/card.templ:
package card
type Variant int
const (
Solid Variant = iota
Outline
Dashed
)
type Props struct {
Class string
Variant Variant
}
templ Card(props Props) {
// ...
}
type TitleProps struct {
// ...
}
templ Title(props TitleProps) {
// ...
}
type DescriptionProps struct {
// ...
}
templ Description(props DescriptionProps) {
// ...
}
And we might use the component:
import "example.com/org/repo/src/components/card"
templ IndexPage() {
<h1>How it works</h1>
@card.Card(card.Props{}) {
@card.Title(card.TitleProps{}) {
Define adoption milestones
}
@card.Description(card.DescriptionProps{}){
Compose milestones as recipes over ordered API events.
Compute in real time and retroactively over historical logs.
}
}
}
Notice how we group the Title and Description components in the card package.
We might have multiple Title components on our site. The benefit of specifying it in the card package is that we make it explicit that this style of Title is used within the card component.
Grouping the components that are supposed to go together can ensure that we use the correct heading levels and consistent styling every time.
6. Variadic Functions to make Props optional
In the previous example, we had to specify the props for all the components by using empty structs. We can make specifying the props optional and the code cleaner to look at by using variadic functions.
Variadic functions in Go accept an arbitrary number of parameters, and we can use this property to accept no parameters by default.
Let's look at the implementation:
templ Title(props ...TitleProps) {
{{ var p TitleProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<h2 class={ twmerge.Merge("text-4xl", p.Class) }>
{ children... }
</h2>
}
The Title component can now accept any number of TitleProps. Using the pattern for all the components in the card package will allow us to use the card component like:
templ IndexPage() {
<h1>How it works</h1>
@card.Card() {
@card.Title() {
Define adoption milestones
}
@card.Description(){
Compose milestones as recipes over ordered API events.
Compute in real time and retroactively over historical logs.
}
}
}
The pattern makes reading the code easier. However, it comes at the cost of a potentially confusing API, where it may be unclear why the components accept multiple values for their Props.
Conclusion & Inspiration
We started with a simple Button component and used the Templ feature of template composition to make our component more extensible. Then we examined how to utilize Go structs and custom types to simplify component configuration with Props. We saw how the tailwind-merge-go package helps us avoid style conflicts. And finally, for the Card component, we used Go packages and variadic functions, both of which enhance the usability and readability of the component.
I picked up many of these patterns from reading the documentation and source code of templUI, a customizable UI kit for templ.