Introduction to Twerge

Twerge is a Go library designed to enhance your experience working with Tailwind CSS in Go applications. The name "Twerge" comes from "Tailwind + Merge".

What is Twerge?

Twerge is a comprehensive Go library that performs four key functions for Tailwind CSS integration:

  1. Intelligent Class Merging - Resolves conflicts between Tailwind CSS classes according to their specificity rules
  2. Class Name Generation - Creates short, unique CSS class names based on hashes of the merged classes
  3. Class Mapping Management - Maintains mappings between original class strings and generated class names, with code generation capabilities

Why Use Twerge?

If you're developing Go-based web applications with Tailwind CSS, Twerge offers significant advantages:

  • Smaller HTML output - By merging conflicting classes and generating short class names
  • Better performance - Through intelligent caching and efficient lookups
  • Build-time optimization - Via code generation capabilities
  • Runtime flexibility - Through the runtime static hashmap for dynamic class handling
  • Simplified workflow - By integrating seamlessly with Go templates, particularly templ

Key Features

  • Intelligent class merging - Resolves conflicts according to Tailwind CSS specificity rules
  • Short class name generation - Creates compact, unique class names for reduced HTML size
  • Runtime class management - Provides a fast lookup system for dynamic applications
  • Code generation - Produces optimized Go code for class mappings
  • CSS integration - Works with Tailwind CLI and CSS build pipelines
  • Flexible configuration - Customizable caching, hash algorithms, and more
  • Nix integration - Reproducible development environment

Target Use Cases

Twerge is particularly well-suited for:

  • Go web applications using Tailwind CSS
  • Projects using the templ templating language
  • Applications requiring build-time CSS optimization
  • Static site generators with Tailwind CSS integration

Next Steps

To get started with Twerge, check out:

Installation

Using Go

go get github.com/conneroisu/twerge

Using Git

You can also clone the repository directly:

git clone https://github.com/conneroisu/twerge.git
cd twerge

Nix Integration (Development Environment)

Twerge comes with Nix support for a reproducible development environment:

# Start a nix shell with all dependencies
nix-shell

# Or with nix flakes
nix develop

Verifying Installation

To verify the installation, create a simple Go program:

package main

import (
    "fmt"
    "github.com/conneroisu/twerge"
)

func main() {
    // Merge conflicting Tailwind classes
    merged := twerge.Merge("text-red-500 bg-blue-300 text-xl")
    fmt.Println(merged) // Should output "bg-blue-300 text-xl text-red-500" or similar order
}

Run the program:

go run main.go

Configuration

Twerge can be configured in several ways to customize its behavior. The library doesn't require a configuration file by default, but you can configure its behavior programmatically. Most of the configuration options are optional and rely on interfaces to provide flexibility.

Merging Tailwind CSS Classes

One of the core features of Twerge is the ability to intelligently merge Tailwind CSS classes, resolving conflicts according to Tailwind's specificity rules.

The Problem

When working with Tailwind CSS, you often need to combine multiple sets of classes, which can lead to conflicts. For example:

// These classes conflict (two text colors)
"text-red-500 text-blue-500"

In this case, you want the last class to win (text-blue-500), following Tailwind's specificity rules.

How Twerge Solves It

The Merge function in Twerge intelligently combines Tailwind classes, resolving conflicts correctly:

import "github.com/conneroisu/twerge"

// Example usage
mergedClasses := twerge.Merge("text-red-500 bg-blue-300 text-xl")

// Result: "bg-blue-300 text-xl text-red-500"
// Classes are resolved and ordered by type

Class Resolution Rules

Twerge follows Tailwind's conflict resolution rules:

  1. Last Declaration Wins - For conflicting classes of the same type, the last one in the string takes precedence
  2. Type Preservation - Non-conflicting classes are preserved
  3. Order Optimization - The resulting class string is optimized for readability and consistency

Supported Class Categories

Twerge understands and correctly handles conflicts between these Tailwind categories:

  • Layout (display, position, etc.)
  • Flexbox & Grid
  • Spacing (margin, padding)
  • Sizing (width, height)
  • Typography (font, text)
  • Backgrounds
  • Borders
  • Effects
  • Filters
  • Tables
  • Transitions & Animation
  • Transforms
  • Interactivity
  • SVG
  • Accessibility

Advanced Merging

Twerge can handle complex combinations, including:

// Merging multiple complex class sets
classes1 := "flex items-center space-x-4 text-sm"
classes2 := "grid text-lg font-bold"
merged := twerge.Merge(classes1 + " " + classes2)

// Result includes non-conflicting classes and resolves conflicts
// "items-center space-x-4 grid text-lg font-bold"

Performance Optimization

Twerge uses an LRU cache for frequently used class combinations:

// Subsequent calls with the same input use the cache
result1 := twerge.Merge("p-4 m-2 p-8")  // Computed
result2 := twerge.Merge("p-4 m-2 p-8")  // Retrieved from cache

Integration Examples

In Go-templ templates, you can use it like this:

// In a templ file
<div class={ twerge.Merge("bg-blue-500 p-4 bg-red-500") }>
  This will have a red background with padding
</div>
  • Merge(classes string) string - Merges Tailwind classes
  • ConfigureCache(size int) - Configures the cache size for merging operations
  • DisableCache() - Disables caching for merging operations

Generating Short Class Names

Twerge can generate short, unique class names based on hashes of the merged Tailwind classes, allowing for smaller HTML output and improved performance.

The Problem

Tailwind CSS is incredibly powerful, but it can lead to long class strings:

<div
  class="flex flex-col items-center justify-between p-4 rounded-lg shadow-lg bg-white hover:bg-gray-50 dark:bg-gray-800 dark:hover:bg-gray-700 transition-all duration-300"
>
  <!-- Content -->
</div>

These long strings increase HTML size, reduce readability, and can impact performance.

How Twerge Solves It

The Generate function creates short, unique class names based on the hash of the merged Tailwind classes:

import "github.com/conneroisu/twerge"

// Long class string
classes := "flex flex-col items-center justify-between p-4 rounded-lg shadow-lg bg-white hover:bg-gray-50"

// Generate a short unique class name
shortClassName := twerge.It(classes)

// Result: "tw-1" (example - actual output will vary)

Benefits of Generated Class Names

  • Smaller HTML - Dramatically reduces HTML file size
  • Better Caching - Improves browser caching of HTML
  • Consistent Naming - Same class string always generates the same short name
  • Collision-Free - Hash-based generation ensures uniqueness
  • Automatic Conflict Resolution - Classes are merged before hashing

How It Works

  1. First, Twerge merges the provided classes to resolve conflicts
  2. The merged class string is hashed using a fast algorithm (default is FNV-1a)
  3. The hash is converted to a short alphanumeric string
  4. A prefix is added (default is "tw-")
  5. The mapping from original classes to the generated name is stored

Customizing Generation

Integration Example

In a Go-templ template:

// Instead of this with long class strings
<div class="flex items-center justify-between p-4 bg-white dark:bg-gray-800">...</div>

// You can do this with a short generated class
<div class={ twerge.It("flex items-center justify-between p-4 bg-white dark:bg-gray-800") }>...</div>

// Which results in HTML like:
// <div class="tw-1">...</div>
// <div class="tw-2">...</div>

Performance Considerations

  • Generated class names are cached for performance
  • The same input always produces the same output
  • Short names reduce HTML size and parsing time

Code Generation

Twerge provides powerful code generation capabilities, allowing you to generate Go code from class mappings for improved performance and type safety.

The Problem

When using shortened class names in a production environment, you need:

  1. A way to consistently map original Tailwind classes to short names
  2. Fast lookups without runtime overhead
  3. Type safety and compiler checks
  4. Integration with build processes

How Twerge Solves It

Twerge can generate Go code that contains the class mappings, providing compile-time checking and improved performance:

package main

import "github.com/conneroisu/twerge"

func main() {
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		"input.css",
		"classes/classes.html",
		views.View(),
	); err != nil {
		panic(err)
	}
}

Benefits of Code Generation

Using generated code provides several advantages:

  1. Performance - No runtime hash computation or map lookups
  2. Type Safety - Compile-time checking of class names
  3. Smaller Binary - Compiled code can be optimized by the Go compiler
  4. Build-time Validation - Issues are caught during the build process
  5. IDE Support - Auto-completion and refactoring support in IDEs

Basic Examples

This page provides basic examples of how to use Twerge in your Go applications.

Simple Website Example

The "simple" example demonstrates a basic website layout with header, main content, and footer sections. It shows how to use Twerge to optimize Tailwind CSS classes in a Go templ application.

Project Structure

simple/
├── _static/
│   └── dist/           # Directory for compiled CSS
├── classes/
│   ├── classes.go      # Generated Go code with class mappings
│   └── classes.html    # HTML output of class definitions 
├── gen.go              # Code generation script
├── go.mod              # Go module file
├── input.css           # TailwindCSS input file
├── main.go             # Web server
├── tailwind.config.js  # TailwindCSS configuration
└── views/
    ├── view.templ      # Templ template file
    └── view_templ.go   # Generated Go code from templ

Code Generation

The gen.go file handles Twerge code generation and TailwindCSS processing:

//go:build ignore
// +build ignore

package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"time"

	"github.com/conneroisu/twerge"
	"github.com/conneroisu/twerge/examples/simple/views"
)

var cwd = flag.String("cwd", "", "current working directory")

func main() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(update-css) Done in %s.\n", elapsed)
	}()
	flag.Parse()
	if *cwd != "" {
		err := os.Chdir(*cwd)
		if err != nil {
			panic(err)
		}
	}
	fmt.Println("Updating Generated Code...")
	start = time.Now()
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		"input.css",
		"classes/classes.html",
		views.View(),
	); err != nil {
		panic(err)
	}
	fmt.Println("Done Generating Code. (took", time.Since(start), ")")

	fmt.Println("Running Tailwind...")
	start = time.Now()
	runTailwind()
	fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}

func runTailwind() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(tailwind) Done in %s.\n", elapsed)
	}()
	cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

Template Usage

The view.templ file shows how to use Twerge in a templ template:

package views

import "github.com/conneroisu/twerge"

templ View() {
	<!DOCTYPE html>
	<html lang="en">
		<head>
			<meta charset="UTF-8"/>
			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
			<title>stellar</title>
			<link rel="stylesheet" href="/dist/styles.css"/>
		</head>
		<body class={ twerge.It("bg-gray-50 text-gray-900 flex flex-col min-h-screen") }>
			<header class={ twerge.It("bg-indigo-600 text-white shadow-md") }>
				<!-- Header content -->
			</header>
			<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
				<!-- Page content -->
			</main>
			<footer class={ twerge.It("bg-gray-800 text-white py-6") }>
				<!-- Footer content -->
			</footer>
		</body>
	</html>
}

Benefits Demonstrated

  • Class Optimization - Long Tailwind class strings are converted to short, efficient class names
  • Build Integration - Twerge integrates with the build process to generate optimized CSS
  • Maintainability - Templates remain readable with full Tailwind class names
  • Performance - Final HTML output uses short class names for improved performance

Running the Example

  1. Navigate to the example directory:
cd examples/simple
  1. Generate the templ components:
templ generate ./views
  1. Run the code generation:
go run gen.go
  1. Run the server:
go run main.go
  1. Open your browser and navigate to http://localhost:8080

Multiple Component Example

For applications with multiple components, you can pass all components to the CodeGen function:

if err := twerge.CodeGen(
	twerge.Default(),
	"classes/classes.go",
	"input.css", 
	"classes/classes.html",
	views.Header(),
	views.Footer(),
	views.Sidebar(),
	views.Content(),
); err != nil {
	panic(err)
}

This ensures all components' Tailwind classes are included in the optimization process.

Web Server Integration

This page demonstrates how to integrate Twerge with a Go web server for delivering optimized Tailwind CSS in a production environment.

HTTP Server Example

The example below shows how to set up a simple HTTP server that serves a web application with Twerge-optimized Tailwind CSS classes.

Server Setup

package main

import (
	"fmt"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"time"

	"github.com/conneroisu/twerge/examples/simple/views"
)

func main() {
	// Serve static files
	fs := http.FileServer(http.Dir("_static"))
	http.Handle("/dist/", http.StripPrefix("/dist/", fs))
	
	// Index route
	http.HandleFunc("/", handleIndex)
	
	// Start server
	port := getEnv("PORT", "8080")
	log.Printf("Server starting on http://localhost:%s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

func handleIndex(w http.ResponseWriter, r *http.Request) {
	// If not at the root path, return 404
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	
	// Render the template
	err := views.View().Render(r.Context(), w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// Helper to get environment variable with default
func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

Middleware for Cache Control

For production, you might want to add cache control headers for better performance:

package main

import (
	"net/http"
	"strings"
	"time"
)

// CacheControlMiddleware adds cache control headers for static assets
func CacheControlMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Set cache headers for static assets
		if strings.HasPrefix(r.URL.Path, "/dist/") {
			w.Header().Set("Cache-Control", "public, max-age=31536000") // 1 year
			w.Header().Set("Expires", time.Now().Add(time.Hour*24*365).Format(time.RFC1123))
		}
		next.ServeHTTP(w, r)
	})
}

// Usage example
func setupServer() {
	staticHandler := http.FileServer(http.Dir("_static"))
	http.Handle("/dist/", CacheControlMiddleware(http.StripPrefix("/dist/", staticHandler)))
}

Template Composition

When building larger applications, you'll want to compose templates. Here's how to use Twerge with template composition:

package views

import "github.com/conneroisu/twerge"

templ Layout(title string) {
	<!DOCTYPE html>
	<html lang="en">
		<head>
			<meta charset="UTF-8"/>
			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
			<title>{ title }</title>
			<link rel="stylesheet" href="/dist/styles.css"/>
		</head>
		<body class={ twerge.It("bg-gray-50 text-gray-900 flex flex-col min-h-screen") }>
			<header class={ twerge.It("bg-indigo-600 text-white shadow-md") }>
				<!-- Header content -->
			</header>
			
			<!-- Slot for page content -->
			{ children... }
			
			<footer class={ twerge.It("bg-gray-800 text-white py-6") }>
				<!-- Footer content -->
			</footer>
		</body>
	</html>
}

// Usage in page templates
templ AboutPage() {
	@Layout("About Us") {
		<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
			<h1 class={ twerge.It("text-3xl font-bold mb-6") }>About Us</h1>
			<p class={ twerge.It("text-gray-700") }>Content goes here...</p>
		</main>
	}
}

API Routes with JSON

If you're building an API that serves both HTML and JSON, you can structure your handlers like this:

package main

import (
	"encoding/json"
	"net/http"
	
	"github.com/conneroisu/twerge/examples/simple/views"
)

// Struct for JSON response
type ApiResponse struct {
	Status  string      `json:"status"`
	Message string      `json:"message"`
	Data    interface{} `json:"data,omitempty"`
}

// Handler that can respond with HTML or JSON
func handleData(w http.ResponseWriter, r *http.Request) {
	data := map[string]interface{}{
		"name": "Twerge Example",
		"description": "A demonstration of Twerge with web servers",
	}
	
	// Check Accept header for JSON request
	if r.Header.Get("Accept") == "application/json" {
		w.Header().Set("Content-Type", "application/json")
		response := ApiResponse{
			Status: "success",
			Data: data,
		}
		json.NewEncoder(w).Encode(response)
		return
	}
	
	// Otherwise render HTML
	err := views.DataView(data).Render(r.Context(), w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

Performance Considerations

When using Twerge with a web server, consider these performance optimizations:

  1. Precompiled Templates: Generate all templ components at build time
  2. Static File Serving: Use proper cache headers for static assets
  3. Compression: Enable gzip/brotli compression for CSS and HTML
  4. CDN Integration: Consider serving static assets from a CDN
package main

import (
	"net/http"
	"strings"
	
	"github.com/NYTimes/gziphandler"
)

func setupCompression() {
	// Apply gzip compression to static assets
	staticHandler := http.FileServer(http.Dir("_static"))
	compressedHandler := gziphandler.GzipHandler(http.StripPrefix("/dist/", staticHandler))
	http.Handle("/dist/", compressedHandler)
}

Example Server with All Features

Here's a complete example integrating all the features mentioned above:

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
	
	"github.com/NYTimes/gziphandler"
	"github.com/conneroisu/twerge/examples/simple/views"
)

func main() {
	// Create router
	mux := http.NewServeMux()
	
	// Static files with compression and caching
	staticHandler := http.FileServer(http.Dir("_static"))
	compressedHandler := gziphandler.GzipHandler(http.StripPrefix("/dist/", staticHandler))
	cachedHandler := CacheControlMiddleware(compressedHandler)
	mux.Handle("/dist/", cachedHandler)
	
	// Routes
	mux.HandleFunc("/", handleIndex)
	mux.HandleFunc("/api/data", handleData)
	
	// Configure server
	server := &http.Server{
		Addr:         ":" + getEnv("PORT", "8080"),
		Handler:      mux,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  120 * time.Second,
	}
	
	// Start server in a goroutine
	go func() {
		log.Printf("Server starting on http://localhost%s", server.Addr)
		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatalf("Server error: %v", err)
		}
	}()
	
	// Graceful shutdown
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit
	
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	
	log.Println("Shutting down server...")
	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("Server forced to shutdown: %v", err)
	}
	
	log.Println("Server gracefully stopped")
}

This comprehensive example demonstrates how to integrate Twerge with a production-ready web server, complete with compression, caching, and graceful shutdown.

Tailwind Build Integration

This page shows how to integrate Twerge with your Tailwind CSS build process for optimal performance and developer experience.

Build Process Overview

A typical Twerge-Tailwind integration includes these steps:

  1. Scan your templates for Tailwind classes
  2. Generate optimized class mappings in Go code
  3. Process CSS with Tailwind CLI
  4. Serve the optimized CSS and HTML

Basic Build Script

Here's a simple build script that handles code generation and Tailwind processing:

//go:build ignore
// +build ignore

package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"time"

	"github.com/conneroisu/twerge"
	"github.com/conneroisu/twerge/examples/simple/views"
)

var cwd = flag.String("cwd", "", "current working directory")

func main() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(update-css) Done in %s.\n", elapsed)
	}()
	flag.Parse()
	if *cwd != "" {
		err := os.Chdir(*cwd)
		if err != nil {
			panic(err)
		}
	}
	fmt.Println("Updating Generated Code...")
	start = time.Now()
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		"input.css",
		"classes/classes.html",
		views.View(),
	); err != nil {
		panic(err)
	}
	fmt.Println("Done Generating Code. (took", time.Since(start), ")")

	fmt.Println("Running Tailwind...")
	start = time.Now()
	runTailwind()
	fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}

func runTailwind() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(tailwind) Done in %s.\n", elapsed)
	}()
	cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

Tailwind Configuration

Here's a sample tailwind.config.js that works well with Twerge:

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './classes/classes.html', // Generated HTML classes from Twerge
    './views/**/*.templ',     // Optional - you can include your templates directly too
  ],
  theme: {
    extend: {
      colors: {
        primary: '#3b82f6',
        secondary: '#10b981',
        accent: '#8b5cf6',
      },
    },
  },
  plugins: [],
}

Development Workflow

For development, you'll want to automatically rebuild when files change. Here's a simple watch script you can use:

//go:build ignore
// +build ignore

package main

import (
	"log"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/fsnotify/fsnotify"
)

func main() {
	watcher, err := fsnotify.NewWatcher()
	if err != nil {
		log.Fatal(err)
	}
	defer watcher.Close()

	// Start the build once at beginning
	runBuild()

	// Watch directories
	dirsToWatch := []string{"./views", "./input.css", "./tailwind.config.js"}
	for _, dir := range dirsToWatch {
		err = watcher.Add(dir)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Watch for changes recursively in views directory
	err = filepath.Walk("./views", func(path string, info os.FileInfo, err error) error {
		if info.IsDir() {
			return watcher.Add(path)
		}
		return nil
	})
	if err != nil {
		log.Fatal(err)
	}

	// Debounce builds
	var lastBuild time.Time
	debounceInterval := 500 * time.Millisecond

	log.Println("Watching for changes...")
	for {
		select {
		case event, ok := <-watcher.Events:
			if !ok {
				return
			}
			// Only rebuild on write or create events for .templ, .css, or .js files
			if (event.Op&fsnotify.Write == fsnotify.Write || event.Op&fsnotify.Create == fsnotify.Create) &&
				(filepath.Ext(event.Name) == ".templ" || filepath.Ext(event.Name) == ".css" || filepath.Ext(event.Name) == ".js") {
				// Debounce
				if time.Since(lastBuild) > debounceInterval {
					lastBuild = time.Now()
					log.Println("Change detected, rebuilding...")
					runBuild()
				}
			}
		case err, ok := <-watcher.Errors:
			if !ok {
				return
			}
			log.Println("error:", err)
		}
	}
}

func runBuild() {
	// First generate templ components
	cmdTempl := exec.Command("templ", "generate", "./views")
	cmdTempl.Stdout = os.Stdout
	cmdTempl.Stderr = os.Stderr
	if err := cmdTempl.Run(); err != nil {
		log.Println("Error generating templ:", err)
		return
	}
	
	// Then run our build script
	cmdGen := exec.Command("go", "run", "gen.go")
	cmdGen.Stdout = os.Stdout
	cmdGen.Stderr = os.Stderr
	if err := cmdGen.Run(); err != nil {
		log.Println("Error running gen.go:", err)
		return
	}
	
	log.Println("Build completed successfully")
}

Production Build Optimization

For production builds, you'll want to minify your CSS and use Tailwind's purge feature to remove unused styles:

//go:build ignore
// +build ignore

package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"time"

	"github.com/conneroisu/twerge"
	"github.com/conneroisu/twerge/examples/simple/views"
)

var (
	cwd = flag.String("cwd", "", "current working directory")
	prod = flag.Bool("prod", false, "production build (minified)")
)

func main() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(update-css) Done in %s.\n", elapsed)
	}()
	flag.Parse()
	if *cwd != "" {
		err := os.Chdir(*cwd)
		if err != nil {
			panic(err)
		}
	}
	
	fmt.Println("Updating Generated Code...")
	start = time.Now()
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		"input.css",
		"classes/classes.html",
		views.View(),
	); err != nil {
		panic(err)
	}
	fmt.Println("Done Generating Code. (took", time.Since(start), ")")

	fmt.Println("Running Tailwind...")
	start = time.Now()
	runTailwind(*prod)
	fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}

func runTailwind(prod bool) {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(tailwind) Done in %s.\n", elapsed)
	}()
	
	args := []string{"-i", "input.css", "-o", "_static/dist/styles.css"}
	if prod {
		args = append(args, "--minify")
	}
	
	cmd := exec.Command("tailwindcss", args...)
	cmd.Env = append(os.Environ(), "NODE_ENV=production")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

Usage:

# Development build
go run gen_prod.go

# Production build (minified)
go run gen_prod.go -prod

Makefile Integration

You can create a Makefile to simplify common build tasks:

.PHONY: dev build watch clean prod

dev:
	templ generate ./views
	go run gen.go

watch:
	go run watch.go

build:
	templ generate ./views
	go run gen.go
	go build -o app ./main.go

prod:
	templ generate ./views
	go run gen_prod.go -prod
	go build -o app -ldflags="-s -w" ./main.go

clean:
	rm -f app
	rm -rf _static/dist/*

GitHub Actions Example

You can automate the build process in CI/CD with GitHub Actions:

name: Build and Deploy

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.24'

    - name: Install templ
      run: go install github.com/a-h/templ/cmd/templ@latest

    - name: Install tailwindcss
      run: npm install -g tailwindcss

    - name: Generate templ components
      run: templ generate ./views

    - name: Build production CSS
      run: go run gen_prod.go -prod

    - name: Build application
      run: go build -o app -ldflags="-s -w" ./main.go

    - name: Upload artifacts
      uses: actions/upload-artifact@v3
      with:
        name: app-build
        path: |
          app
          _static/dist

Docker Integration

For containerized deployments, here's a sample Dockerfile:

# Build stage
FROM golang:1.24-alpine AS builder

# Install Node.js and npm for Tailwind
RUN apk add --no-cache nodejs npm

# Install Tailwind CSS
RUN npm install -g tailwindcss

# Install templ
RUN go install github.com/a-h/templ/cmd/templ@latest

WORKDIR /app

# Copy dependencies first for better caching
COPY go.mod go.sum ./
RUN go mod download

# Copy source files
COPY . .

# Generate templ files
RUN templ generate ./views

# Build with Twerge
RUN go run gen_prod.go -prod

# Build the Go application
RUN CGO_ENABLED=0 GOOS=linux go build -o app -ldflags="-s -w" ./main.go

# Runtime stage
FROM alpine:latest

WORKDIR /app

# Copy only necessary files from the builder stage
COPY --from=builder /app/app .
COPY --from=builder /app/_static/dist _static/dist

# Expose the port the app runs on
EXPOSE 8080

# Run the application
CMD ["./app"]

Multi-Theme Support

You can extend your build script to generate multiple theme variants:

//go:build ignore
// +build ignore

package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	"github.com/conneroisu/twerge"
	"github.com/conneroisu/twerge/examples/simple/views"
)

var (
	cwd = flag.String("cwd", "", "current working directory")
	theme = flag.String("theme", "default", "theme name (default, dark, custom)")
)

func main() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(update-css) Done in %s.\n", elapsed)
	}()
	flag.Parse()
	if *cwd != "" {
		err := os.Chdir(*cwd)
		if err != nil {
			panic(err)
		}
	}
	
	// Determine input file based on theme
	inputCss := "input.css"
	if *theme != "default" {
		inputCss = fmt.Sprintf("input-%s.css", *theme)
	}
	
	// Determine output directory based on theme
	outputDir := filepath.Join("_static", "dist")
	outputCss := filepath.Join(outputDir, "styles.css")
	if *theme != "default" {
		outputCss = filepath.Join(outputDir, fmt.Sprintf("styles-%s.css", *theme))
	}
	
	fmt.Printf("Building theme: %s\n", *theme)
	fmt.Printf("Input CSS: %s\n", inputCss)
	fmt.Printf("Output CSS: %s\n", outputCss)
	
	fmt.Println("Updating Generated Code...")
	start = time.Now()
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		inputCss,
		"classes/classes.html",
		views.View(),
	); err != nil {
		panic(err)
	}
	fmt.Println("Done Generating Code. (took", time.Since(start), ")")

	fmt.Println("Running Tailwind...")
	start = time.Now()
	runTailwind(inputCss, outputCss)
	fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}

func runTailwind(input, output string) {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(tailwind) Done in %s.\n", elapsed)
	}()
	
	// Create output directory if it doesn't exist
	outputDir := filepath.Dir(output)
	if err := os.MkdirAll(outputDir, 0755); err != nil {
		panic(err)
	}
	
	cmd := exec.Command("tailwindcss", "-i", input, "-o", output)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

Usage:

# Build default theme
go run gen_themes.go

# Build dark theme
go run gen_themes.go -theme dark

This example demonstrates how to integrate Twerge into your Tailwind CSS build process for both development and production environments.

Complex Web Application

This page demonstrates using Twerge in a more complex web application scenario, showcasing a dashboard example with multiple components and dynamic data.

Dashboard Example Overview

The dashboard example demonstrates a modern analytics UI with:

  • Responsive layout using Tailwind CSS
  • Multiple templ components for different sections
  • Metrics cards with dynamic data
  • Data tables for displaying information
  • Twerge optimization for class handling

Project Structure

dashboard/
├── _static/
│   └── dist/             # Directory for compiled CSS
├── classes/
│   ├── classes.go        # Generated Go code with class mappings
│   └── classes.html      # HTML output of class definitions 
├── gen.go                # Code generation script
├── go.mod                # Go module file
├── input.css             # TailwindCSS input file
├── main.go               # Web server implementation
├── tailwind.config.js    # TailwindCSS configuration
└── views/
    ├── dashboard.templ   # Dashboard page component
    ├── report.templ      # Report page component
    ├── settings.templ    # Settings page component
    └── view.templ        # Layout component

Code Generation with Multiple Components

The dashboard example uses multiple components, all processed by Twerge:

//go:build ignore
// +build ignore

package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"time"

	"github.com/conneroisu/twerge"
	"github.com/conneroisu/twerge/examples/dashboard/views"
)

var cwd = flag.String("cwd", "", "current working directory")

func main() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(update-css) Done in %s.\n", elapsed)
	}()
	flag.Parse()
	if *cwd != "" {
		err := os.Chdir(*cwd)
		if err != nil {
			panic(err)
		}
	}
	fmt.Println("Updating Generated Code...")
	start = time.Now()
	if err := twerge.CodeGen(
		twerge.Default(),
		"classes/classes.go",
		"input.css",
		"classes/classes.html",
		views.Dashboard(),
		views.Settings(),
		views.Report(),
	); err != nil {
		panic(err)
	}
	fmt.Println("Done Generating Code. (took", time.Since(start), ")")

	fmt.Println("Running Tailwind...")
	start = time.Now()
	runTailwind()
	fmt.Println("Done Running Tailwind. (took", time.Since(start), ")")
}

func runTailwind() {
	start := time.Now()
	defer func() {
		elapsed := time.Since(start)
		fmt.Printf("(tailwind) Done in %s.\n", elapsed)
	}()
	cmd := exec.Command("tailwindcss", "-i", "input.css", "-o", "_static/dist/styles.css")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		panic(err)
	}
}

Dashboard Component

The dashboard component displays metrics cards and a data table:

package views

import "github.com/conneroisu/twerge"

templ Dashboard() {
	@Layout("Dashboard") {
		<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
			<div class={ twerge.It("mb-6") }>
				<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Overview</h2>
				<div class={ twerge.It("grid grid-cols-1 md:grid-cols-3 gap-6") }>
					<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
						<div class={ twerge.It("flex items-center justify-between") }>
							<h3 class={ twerge.It("text-gray-500 text-sm font-medium") }>Total Users</h3>
							<span class={ twerge.It("bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded") }>+12%</span>
						</div>
						<p class={ twerge.It("text-3xl font-bold text-gray-800 mt-2") }>24,521</p>
						<div class={ twerge.It("mt-4 text-sm text-gray-500") }>1,250 new users this week</div>
					</div>
					<!-- Additional metric cards -->
				</div>
			</div>
			<div class={ twerge.It("mb-6") }>
				<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Recent Activity</h2>
				<div class={ twerge.It("bg-white rounded-lg shadow-md overflow-hidden") }>
					<table class={ twerge.It("min-w-full divide-y divide-gray-200") }>
						<!-- Table header and rows -->
					</table>
				</div>
			</div>
		</main>
	}
}

Settings Component

The settings page demonstrates form handling with Tailwind:

package views

import "github.com/conneroisu/twerge"

templ Settings() {
	@Layout("Settings") {
		<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
			<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-6") }>Account Settings</h2>
			
			<div class={ twerge.It("bg-white rounded-lg shadow-md p-6 mb-6") }>
				<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>Profile Information</h3>
				<form>
					<div class={ twerge.It("grid grid-cols-1 gap-6 md:grid-cols-2") }>
						<div>
							<label for="name" class={ twerge.It("block text-sm font-medium text-gray-700 mb-1") }>Name</label>
							<input type="text" id="name" name="name" value="John Doe" 
								class={ twerge.It("w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }/>
						</div>
						<div>
							<label for="email" class={ twerge.It("block text-sm font-medium text-gray-700 mb-1") }>Email</label>
							<input type="email" id="email" name="email" value="john@example.com" 
								class={ twerge.It("w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }/>
						</div>
						<!-- More form fields -->
					</div>
					<div class={ twerge.It("mt-6") }>
						<button type="submit" class={ twerge.It("px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2") }>
							Save Changes
						</button>
					</div>
				</form>
			</div>
			
			<!-- Additional settings sections -->
		</main>
	}
}

Report Component

The report page shows how to present data visualizations:

package views

import "github.com/conneroisu/twerge"

templ Report() {
	@Layout("Analytics Report") {
		<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
			<div class={ twerge.It("flex justify-between items-center mb-6") }>
				<h2 class={ twerge.It("text-2xl font-bold text-gray-800") }>Analytics Report</h2>
				<div class={ twerge.It("flex space-x-2") }>
					<button class={ twerge.It("px-3 py-1 bg-white border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50") }>
						Export PDF
					</button>
					<button class={ twerge.It("px-3 py-1 bg-white border border-gray-300 rounded-md text-sm text-gray-700 hover:bg-gray-50") }>
						Export CSV
					</button>
				</div>
			</div>
			
			<div class={ twerge.It("grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6") }>
				<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
					<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>Revenue Overview</h3>
					<div class={ twerge.It("h-64 bg-gray-100 rounded flex items-center justify-center") }>
						<p class={ twerge.It("text-gray-500") }>Chart Placeholder</p>
					</div>
				</div>
				
				<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
					<h3 class={ twerge.It("text-lg font-medium text-gray-800 mb-4") }>User Growth</h3>
					<div class={ twerge.It("h-64 bg-gray-100 rounded flex items-center justify-center") }>
						<p class={ twerge.It("text-gray-500") }>Chart Placeholder</p>
					</div>
				</div>
			</div>
			
			<!-- Additional report sections -->
		</main>
	}
}

Layout Component with Template Composition

The layout component used by all pages for consistent structure:

package views

import "github.com/conneroisu/twerge"

templ Layout(title string) {
	<!DOCTYPE html>
	<html lang="en">
		<head>
			<meta charset="UTF-8"/>
			<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
			<title>{ title } | Dashboard</title>
			<link rel="stylesheet" href="/dist/styles.css"/>
		</head>
		<body class={ twerge.It("bg-gray-100 text-gray-900 flex") }>
			<!-- Sidebar -->
			<aside class={ twerge.It("hidden md:flex md:flex-col w-64 bg-gray-800 text-white") }>
				<div class={ twerge.It("flex items-center justify-center h-16 border-b border-gray-700") }>
					<h1 class={ twerge.It("text-xl font-bold") }>Dashboard</h1>
				</div>
				<nav class={ twerge.It("flex-grow") }>
					<ul class={ twerge.It("mt-6") }>
						<li>
							<a href="/" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
								<span class={ twerge.It("ml-2") }>Dashboard</span>
							</a>
						</li>
						<li>
							<a href="/reports" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
								<span class={ twerge.It("ml-2") }>Reports</span>
							</a>
						</li>
						<li>
							<a href="/settings" class={ twerge.It("flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700") }>
								<span class={ twerge.It("ml-2") }>Settings</span>
							</a>
						</li>
					</ul>
				</nav>
			</aside>
			
			<!-- Main content -->
			<div class={ twerge.It("flex flex-col flex-grow min-h-screen") }>
				<!-- Top navbar -->
				<header class={ twerge.It("bg-white shadow h-16 flex items-center justify-between px-6") }>
					<div class={ twerge.It("flex items-center") }>
						<button class={ twerge.It("md:hidden mr-4 text-gray-600") }>
							<svg class={ twerge.It("h-6 w-6") } fill="none" viewBox="0 0 24 24" stroke="currentColor">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/>
							</svg>
						</button>
						<h2 class={ twerge.It("text-lg font-medium") }>{ title }</h2>
					</div>
					<div class={ twerge.It("flex items-center") }>
						<button class={ twerge.It("p-1 mr-4 text-gray-500") }>
							<svg class={ twerge.It("h-6 w-6") } fill="none" viewBox="0 0 24 24" stroke="currentColor">
								<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
							</svg>
						</button>
						<div class={ twerge.It("relative") }>
							<button class={ twerge.It("flex items-center") }>
								<img class={ twerge.It("h-8 w-8 rounded-full") } src="https://randomuser.me/api/portraits/men/1.jpg" alt="User profile"/>
								<span class={ twerge.It("ml-2 text-sm") }>John Doe</span>
							</button>
						</div>
					</div>
				</header>
				
				<!-- Page content -->
				{ children... }
			</div>
		</body>
	</html>
}

Web Server Implementation

The main.go file sets up routes for each component:

package main

import (
	"log"
	"net/http"
	"os"
	
	"github.com/conneroisu/twerge/examples/dashboard/views"
)

func main() {
	// Static file handler
	fs := http.FileServer(http.Dir("_static"))
	http.Handle("/dist/", http.StripPrefix("/dist/", fs))
	
	// Routes
	http.HandleFunc("/", handleDashboard)
	http.HandleFunc("/reports", handleReports)
	http.HandleFunc("/settings", handleSettings)
	
	// Start server
	port := getEnv("PORT", "8080")
	log.Printf("Server starting on http://localhost:%s", port)
	if err := http.ListenAndServe(":"+port, nil); err != nil {
		log.Fatal(err)
	}
}

func handleDashboard(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		http.NotFound(w, r)
		return
	}
	
	err := views.Dashboard().Render(r.Context(), w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func handleReports(w http.ResponseWriter, r *http.Request) {
	err := views.Report().Render(r.Context(), w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

func handleSettings(w http.ResponseWriter, r *http.Request) {
	err := views.Settings().Render(r.Context(), w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

// Helper to get environment variable with default
func getEnv(key, fallback string) string {
	if value, ok := os.LookupEnv(key); ok {
		return value
	}
	return fallback
}

Dynamic Data Integration

In a real application, you'd likely integrate with a database or API. Here's an example of how to pass dynamic data to templates:

package models

type User struct {
	ID       int
	Name     string
	Email    string
	Role     string
	Avatar   string
	LastSeen string
}

type MetricCard struct {
	Title    string
	Value    string
	Change   string
	IsUp     bool
	Subtitle string
}

type Order struct {
	ID       string
	Customer string
	Amount   string
	Status   string
	Date     string
}

// Repository interface
type DataRepository interface {
	GetUsers() []User
	GetMetrics() []MetricCard
	GetRecentOrders() []Order
}
package data

import "github.com/example/dashboard/models"

type Repository struct {
	// Database connection or other dependencies
}

func NewRepository() *Repository {
	return &Repository{}
}

func (r *Repository) GetMetrics() []models.MetricCard {
	return []models.MetricCard{
		{
			Title:    "Total Users",
			Value:    "24,521",
			Change:   "+12%",
			IsUp:     true,
			Subtitle: "1,250 new users this week",
		},
		{
			Title:    "Total Revenue",
			Value:    "$45,428",
			Change:   "+5%",
			IsUp:     true,
			Subtitle: "$2,150 new revenue this week",
		},
		{
			Title:    "Total Orders",
			Value:    "12,234",
			Change:   "-2%",
			IsUp:     false,
			Subtitle: "345 new orders this week",
		},
	}
}

// Implement other methods...
package views

import (
	"github.com/conneroisu/twerge"
	"github.com/example/dashboard/models"
)

templ MetricCard(card models.MetricCard) {
	<div class={ twerge.It("bg-white rounded-lg shadow-md p-6") }>
		<div class={ twerge.It("flex items-center justify-between") }>
			<h3 class={ twerge.It("text-gray-500 text-sm font-medium") }>{ card.Title }</h3>
			if card.IsUp {
				<span class={ twerge.It("bg-green-100 text-green-800 text-xs font-semibold px-2 py-1 rounded") }>{ card.Change }</span>
			} else {
				<span class={ twerge.It("bg-red-100 text-red-800 text-xs font-semibold px-2 py-1 rounded") }>{ card.Change }</span>
			}
		</div>
		<p class={ twerge.It("text-3xl font-bold text-gray-800 mt-2") }>{ card.Value }</p>
		<div class={ twerge.It("mt-4 text-sm text-gray-500") }>{ card.Subtitle }</div>
	</div>
}

templ Dashboard(metrics []models.MetricCard, orders []models.Order) {
	@Layout("Dashboard") {
		<main class={ twerge.It("container mx-auto px-4 py-6 flex-grow") }>
			<div class={ twerge.It("mb-6") }>
				<h2 class={ twerge.It("text-2xl font-bold text-gray-800 mb-4") }>Overview</h2>
				<div class={ twerge.It("grid grid-cols-1 md:grid-cols-3 gap-6") }>
					for _, metric := range metrics {
						@MetricCard(metric)
					}
				</div>
			</div>
			<!-- Table with orders... -->
		</main>
	}
}

Benefits of This Approach

The complex dashboard example demonstrates several benefits of using Twerge:

  1. Component Reusability - Common UI elements can be extracted into reusable components
  2. Optimized Output - Large Tailwind class strings are converted to short, efficient codes
  3. Type Safety - Generated Go code provides compile-time checking
  4. Performance - HTML output is smaller and faster to parse
  5. Maintainability - Templates remain readable with full Tailwind class names
  6. Dynamic Data Integration - Easy to integrate with databases or APIs

Running the Example

  1. Navigate to the example directory:
cd examples/dashboard
  1. Generate the templ components:
templ generate ./views
  1. Run the code generation:
go run gen.go
  1. Run the server:
go run main.go
  1. Open your browser and navigate to http://localhost:8080

The dashboard example demonstrates how Twerge can be used in complex web applications with multiple components, layouts, and dynamic data integration.

Frequently Asked Questions

This page answers common questions about using Twerge with Tailwind CSS and Go.

General Questions

What is Twerge?

Twerge is a Go library that optimizes Tailwind CSS class usage in Go web applications. It provides class merging, short class name generation, and code generation features to improve performance and developer experience.

Why should I use Twerge?

Twerge solves several common challenges when working with Tailwind CSS:

  • It correctly merges Tailwind classes, resolving conflicts according to Tailwind's specificity rules
  • It generates short, unique class names to reduce HTML size
  • It creates Go code with class mappings for improved performance and type safety
  • It integrates smoothly with Go build processes and templ templates

How does Twerge compare to similar tools?

Unlike JavaScript-based tools like tailwind-merge or clsx, Twerge is designed specifically for Go applications. It integrates natively with Go code, templ templates, and build processes to provide a seamless development experience.

Technical Questions

Does Twerge support all Tailwind CSS features?

Twerge supports all standard Tailwind CSS classes, including:

  • Layout and positioning
  • Flexbox and Grid
  • Typography
  • Colors and backgrounds
  • Borders and shadows
  • Transitions and animations
  • Responsive variants
  • Dark mode variants
  • Hover, focus, and other state variants

For custom Tailwind plugins or very specific edge cases, check the documentation or submit an issue on GitHub.

How does class conflict resolution work?

Twerge follows Tailwind's own conflict resolution rules:

  1. The last conflicting class wins (e.g., text-red-500 text-blue-500 results in blue text)
  2. Classes are grouped by category for readability and optimization
  3. An internal mapping handles all standard Tailwind class conflicts

What's the performance impact?

Twerge is designed for performance:

  • Class merging uses an LRU cache for frequently used combinations
  • Generated code offers zero runtime overhead for class lookups
  • Short class names reduce HTML size and improve parsing time
  • The build-time approach means no client-side JavaScript overhead

Tests show that using Twerge can reduce HTML size by 30-50% for Tailwind-heavy pages.

Can I use Twerge with existing projects?

Yes, you can integrate Twerge into existing Go web projects that use Tailwind CSS. The integration process is straightforward:

  1. Install Twerge: go get github.com/conneroisu/twerge
  2. Update your templates to use twerge.It() or twerge.Merge()
  3. Set up code generation in your build process
  4. Configure TailwindCSS to use the generated HTML classes

See the Examples section for detailed integration guides.

Common Issues

Classes aren't being applied correctly

If classes aren't being applied correctly, check:

  1. Make sure the generated CSS is being included in your HTML
  2. Verify that your templates are using twerge.It() correctly
  3. Check that code generation is running before the Tailwind build
  4. Inspect the generated classes.html file to ensure your classes are included

Build errors in code generation

If you're seeing build errors:

  1. Ensure you're using the latest version of Twerge
  2. Check that your templ templates are valid and compiling correctly
  3. Verify that the paths in your CodeGen function are correct
  4. Check the generated code for any issues

Performance concerns

If you're concerned about performance:

  1. Use the code generation approach for production builds
  2. Configure appropriate cache sizes for your application
  3. Consider enabling minification in your Tailwind build
  4. Use HTTP compression for serving HTML and CSS

Usage Examples

How do I merge classes conditionally?

// Conditional class merging
func Button(primary bool) templ.Component {
    classes := "px-4 py-2 rounded"
    if primary {
        classes = twerge.Merge(classes + " bg-blue-600 text-white")
    } else {
        classes = twerge.Merge(classes + " bg-gray-200 text-gray-800")
    }
    
    return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
        return templ.Tag("button", templ.Attributes{
            "class": classes,
        }).Render(ctx, w)
    })
}

How do I integrate with CI/CD pipelines?

For CI/CD pipelines, include the Twerge code generation step in your build process:

# GitHub Actions example
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Go
        uses: actions/setup-go@v4
        with:
          go-version: '1.24'
      - name: Install dependencies
        run: |
          go install github.com/a-h/templ/cmd/templ@latest
          npm install -g tailwindcss
      - name: Generate templ code
        run: templ generate ./views
      - name: Run Twerge code generation
        run: go run ./gen.go
      - name: Build application
        run: go build -o app

Getting Help

If you have questions not covered in this FAQ:

  1. Check the documentation
  2. Search for existing issues
  3. Open a new issue if you think you've found a bug
  4. Join the community discussions on GitHub or Discord

Acknowledgements

Original Works

Special thanks to Oudwins for his work on the original tailwind-merge-go:

https://github.com/Oudwins/tailwind-merge-go

Projects That Inspired Twerge

  • Tailwind CSS - The utility-first CSS framework that makes rapid UI development possible
  • tailwind-merge - The original JavaScript library for merging Tailwind CSS classes
  • tailwind-merge-go - Go port of the tailwind-merge library
  • templ - HTML templating language for Go that inspired Twerge's integration patterns

Libraries Used

  • Jennifer - Used for code generation capabilities
  • testify - For assertions in tests
  • Various Go standard library packages

Contributors

Thank you to all contributors who have helped improve Twerge through bug reports, feature suggestions, and code contributions.

Community

Thanks to the broader Go and Tailwind CSS communities for creating an ecosystem where tools like Twerge can thrive.