Skip to content

CoreyCole/datastarui

Repository files navigation

DatastarUI

A Go/templ port of shadcn/ui components that maintains pixel-perfect visual and behavioral parity with minimal JavaScript (lightweight 15KB Datastar library for reactivity).

See datastar-ui.com for component demos.

✨ Features

  • πŸš€ Server-side rendered components with Go/templ
  • ⚑ Reactive UI powered by Datastar signals
  • 🎨 Identical styling to shadcn/ui using Tailwind CSS
  • πŸ“¦ Lightweight - only 15KB Datastar runtime
  • πŸ”§ Type-safe component args with Go structs
  • πŸŒ™ Dark mode support built-in
  • β™Ώ Accessible with proper ARIA attributes
  • ⌨️ Keyboard support to tab through forms, arrow keys, enter to select, escape to close, etc.

πŸš€ Quick Start

Prerequisites

Development Setup

# start the Tailwind CSS watcher:
just tailwind

# start the Go server with live reload:
just watch

see demo site at http://localhost:4242

The development environment will automatically:

  • βœ… Rebuild Go templates when .templ files change
  • βœ… Recompile CSS when Tailwind classes are added/removed
  • βœ… Restart the server when Go code changes

πŸ—οΈ Project Structure

datastarui/
β”œβ”€β”€ components/          # Reusable UI components
β”‚   β”œβ”€β”€ button/          # Button component
β”‚   β”‚   β”œβ”€β”€ button.templ # Template file
β”‚   β”‚   β”œβ”€β”€ args.go      # Component arguments
β”‚   β”‚   └── variants.go  # CSS class variants
β”‚   └── select/          # Select component (fully refactored)
β”œβ”€β”€ utils/               # Utility libraries
β”‚   β”œβ”€β”€ signals.go       # Signal management with namespacing
β”‚   β”œβ”€β”€ expressions.go   # Datastar expression builders
β”‚   └── data_class.go    # Conditional CSS class helpers
β”œβ”€β”€ pages/               # Page templates
β”‚   └── components/      # Component demo pages
└── main.go              # Server entry point

🧩 Component Architecture

Each component follows a pattern with utility-driven Datastar integration:

Template File (dialog.templ)

package dialog

import "github.com/coreycole/datastarui/utils"

// DialogSignals defines the signal structure for dialog components
type DialogSignals struct {
	Open bool `json:"open"`
}

// Dialog container - pure Datastar signal-based modal using data-show
templ Dialog(args DialogArgs) {
	{{
		// Create signals using the new structured system with proper initial state
		signals := utils.Signals(args.ID, DialogSignals{
			Open: args.DefaultOpen,
		})

		// Dialog backdrop overlay
		backdropClasses := "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"

		// Dialog positioning classes - centered on screen
		dialogClasses := "fixed left-[50%] top-[50%] z-50 w-full max-w-lg translate-x-[-50%] translate-y-[-50%] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95"

		// Generate the CSS classes for the inner dialog content container using our variant system
		containerClasses := DialogVariants(args)

		// Create dialog handler for clean expressions
		dialogHandler := NewDialogHandler(signals)
		backdropClickHandler := dialogHandler.BuildBackdropClickHandler()
		escapeHandler := dialogHandler.BuildEscapeHandler()
	}}
	<div data-signals={ signals.DataSignals }>
		<!-- Dialog backdrop overlay -->
		<div
			data-show={ signals.Signal("open") }
			class={ backdropClasses }
			data-on-click={ backdropClickHandler }
			data-on-keydown__window={ escapeHandler }
			if !args.DefaultOpen {
				style="display: none;"
			}
		>
			<!-- Dialog content container -->
			<div
				id={ args.ID }
				class={ dialogClasses }
				role="dialog"
				aria-modal="true"
				tabindex="-1"
				data-on-click="evt.stopPropagation()"
				data-on-mount="evt.target.focus()"
			>
				<div class={ containerClasses }>
					{ children... }
				</div>
			</div>
		</div>
	</div>
}

Expressions (expressions.go)

package dialog

import (
	"fmt"
	"github.com/coreycole/datastarui/utils"
)

// DialogHandler creates handlers for Dialog component functionality
type DialogHandler struct {
	signals *utils.SignalManager
}

// NewDialogHandler creates a dialog handler
func NewDialogHandler(signals *utils.SignalManager) *DialogHandler {
	return &DialogHandler{
		signals: signals,
	}
}

// BuildBackdropClickHandler creates the backdrop click handler for closing dialog
func (d *DialogHandler) BuildBackdropClickHandler() string {
	return d.signals.ConditionalAction("evt.target === evt.currentTarget", "open", "false")
}

// BuildEscapeHandler creates an escape key handler for closing dialog
func (d *DialogHandler) BuildEscapeHandler() string {
	condition := fmt.Sprintf("evt.key === 'Escape' && %s", d.signals.Signal("open"))
	return d.signals.ConditionalAction(condition, "open", "false")
}

// BuildCloseHandler creates a close handler with optional return value
func (d *DialogHandler) BuildCloseHandler(returnValue string) string {
	expr := utils.NewExpression().Statement(d.signals.Set("open", "false"))
	
	if returnValue != "" {
		expr.Statement(d.signals.SetString("returnValue", returnValue))
	}
	
	return expr.Build()
}

Args Definition (args.go)

package dialog

import "github.com/a-h/templ"

// DialogArgs defines the args for the Dialog container (using Datastar signals)
type DialogArgs struct {
	ID          string
	DefaultOpen bool // Whether the dialog should be open by default
	Class       string
	Attributes  templ.Attributes
}

// DialogTriggerArgs defines the args for the DialogTrigger component
type DialogTriggerArgs struct {
	DialogID   string
	AsChild    bool
	Class      string
	Attributes templ.Attributes
}

CSS Variants (variants.go)

package dialog

import (
	"github.com/coreycole/datastarui/utils"
)

// DialogVariants returns the CSS classes for the main Dialog container component
func DialogVariants(args DialogArgs) string {
	// Dialog-specific styling - optimized for modal dialogs with consistent padding
	baseClasses := "max-w-lg w-full max-h-[90vh] overflow-auto bg-background border shadow-lg rounded-lg p-6"

	return utils.TwMerge(baseClasses, args.Class)
}

🎨 Design System

The project uses tailwind classes from shadcn/ui components new york v4.

🀝 Contributing

  1. Pick a component from the shadcn/ui registry
  2. Follow the utility-driven architecture using expression builders
  3. Create comprehensive demos showing all variants

πŸ“– Documentation

πŸ“„ License

MIT License - see LICENSE for details.

About

Templ components built with Datastar and Tailwind

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •