vee is a bi-directional binding and rendering library for Go structs as HTML forms. It generates HTML forms from Go struct definitions and parses form data back into structs with validation.
- Framework Agnostic: Works with standard
net/http
and any web framework - Simple: Flat structs only, no nested structures
- Convention-based: Uses naming patterns for complex behaviors
- Tag-driven: Configuration through struct tags
string
→<input type="text">
int
,int64
→<input type="number">
float64
→<input type="number" step="any">
bool
→<input type="checkbox">
time.Time
→<input type="datetime-local">
time.Duration
→<input type="number">
(with units field)
Pointer types indicate optional fields and support all base types:
*string
→<input type="text">
(empty value for nil)*int
,*int64
→<input type="number">
(zero value for nil)*float64
→<input type="number" step="any">
(zero value for nil)*bool
→<input type="checkbox">
(unchecked for nil)*time.Time
→<input type="datetime-local">
(no value attribute for nil)*time.Duration
→<input type="number">
(no value attribute for nil)
Rendering Behavior:
- Nil pointer: Field rendered without value (or zero value for numeric types)
- Non-nil pointer: Field rendered with pointer's value
- All pointer fields are always rendered - nil vs non-nil only affects the value
Pointer types have subtle but important behavioral differences from their non-pointer equivalents, especially during form binding.
For *string
, *int
, *int64
, *float64
, *time.Time
, and *time.Duration
:
type User struct {
Name string // Regular field
Email *string // Pointer field
Age int // Regular field
Score *float64 // Pointer field
}
Form Binding Behavior:
- Field present in form data: Creates new pointer and sets value
- Field absent from form data: Leaves pointer unchanged (preserves existing value)
// Starting values
user := User{
Name: "John",
Email: stringPtr("[email protected]"),
Age: 30,
Score: nil,
}
// Form data: {"name": ["Jane"], "age": ["25"]}
// Email and Score are missing from form
vee.Bind(formData, &user)
// Result:
// user.Name = "Jane" (updated from form)
// user.Email = "[email protected]" (unchanged - preserved)
// user.Age = 25 (updated from form)
// user.Score = nil (unchanged - still nil)
Boolean pointer types (*bool
) follow HTML checkbox semantics, which are different:
type Settings struct {
IsActive bool // Regular checkbox
IsOptional *bool // Pointer checkbox
}
Form Binding Behavior:
- Checkbox checked (field present): Sets pointer to
&true
- Checkbox unchecked (field absent): Sets pointer to
&false
Important: Unlike other pointer types, boolean pointers do NOT preserve existing values when absent from form data. This follows standard HTML checkbox behavior where unchecked boxes don't send any data.
// Starting values
settings := Settings{
IsActive: true,
IsOptional: boolPtr(true), // Previously checked
}
// Form data: {"is_active": ["true"]}
// is_optional is missing (checkbox was unchecked)
vee.Bind(formData, &settings)
// Result:
// settings.IsActive = true (checked - present in form)
// settings.IsOptional = &false (unchecked - absent from form)
HTML checkboxes have unique behavior:
- Checked checkbox: Browser sends field in form data
- Unchecked checkbox: Browser sends NO data for that field
For regular fields, "no data" means "don't change the value". But for checkboxes, "no data" explicitly means "unchecked" (false). This creates the behavioral difference between boolean pointers and other pointer types.
Optional String Fields:
type Profile struct {
Name string `vee:"required"` // Always required
Bio *string `vee:"placeholder:'Optional bio'"` // Optional
Website *string `vee:"type:'url'"` // Optional URL
}
Optional Numeric Fields:
type Product struct {
Name string `vee:"required"`
Price float64 `vee:"required,min:0"`
Sale *float64 `vee:"min:0"` // Optional sale price
}
Settings with Optional Toggles:
type UserSettings struct {
EmailNotifications bool // Default behavior
SmsNotifications *bool // Optional setting (nil = not configured)
}
`vee:"[${override_name},]param1,param2:value,param3:'quoted string'"`
Override the HTML form field name using $
prefix as the first parameter:
FirstName string `vee:"$firstName,required,label:'First Name'"`
All string values must be wrapped in single quotes:
Name string `vee:"label:'Full Name',placeholder:'Enter your name',help:'This is required'"`
CSS classes are passed directly to the HTML class
attribute:
Name string `vee:"required" css:"border-2 border-gray-300 rounded px-3 py-2"`
Available for all field types:
required
- Adds HTMLrequired
attribute for client-side validation (see Validation section for server-side validation)readonly
- Field is read-onlydisabled
- Field is disabledhidden
- Renders as<input type="hidden">
without label (not supported for pointer types or multi-value fields)label:'Text'
- Custom label text (defaults to human-readable field name)nolabel
- Skip automatic label generationplaceholder:'Text'
- Placeholder text (forces rendering for pointer types)help:'Text'
- Help/description textid:'custom_id'
- Custom HTML id (always defaults to field name if not specified)
Name string `vee:"type:'email'"`
type:'email|password|tel|url'
- HTML input type override
Age int `vee:"step:1"`
Price float64 `vee:"step:0.01"`
step:N
- Step increment for HTML input
Active bool `vee:"label:'Is Active'"`
Note: Boolean fields are rendered as checkboxes with value="true"
. The checked
attribute is set based on the struct field value - no tag override is needed.
Birthday time.Time `vee:"type:'date'"`
type:'date|datetime-local|time'
- HTML input type (defaults to datetime-local)
Timeout time.Duration `vee:"units:'s',label:'Timeout'"`
units:'ms|s|m|h'
- Duration units (milliseconds, seconds, minutes, hours, defaults to seconds)
Rendering: Creates a number input with the value converted to the specified units.
Binding: Converts the number back to time.Duration
using the units.
vee integrates with go-playground/validator for validation. Use standard validate
tags alongside vee
tags:
type User struct {
Name string `vee:"required" validate:"required,min=2,max=50"`
Email string `vee:"type:'email',required" validate:"required,email"`
Age int `validate:"required,gte=18,lte=120"`
Phase int `vee:"hidden" validate:"required"`
}
// Validate the struct
user := User{Name: "John", Email: "[email protected]", Age: 25, Phase: 1}
if err := vee.Validate(user); err != nil {
// Handle validation errors
}
Available Functions:
vee.Validate(struct)
- Validates a struct using validator tagsvee.ValidateVar(value, tag)
- Validates a single value
Important Distinction:
- vee's
required
attribute: Only affects HTML form generation by adding therequired
attribute to input elements for client-side validation - Validator's
required
tag: Handles actual server-side validation logic
type Examples struct {
// Client + server validation
Name string `vee:"required" validate:"required"`
// Only client-side (HTML required attribute)
Email string `vee:"required"`
// Only server-side validation
Age int `validate:"required"`
// Hidden field with server validation but no HTML required
Phase int `vee:"hidden" validate:"required"`
}
Note: vee handles form rendering and binding, while validator handles validation logic. This separation keeps each library focused on its strengths.
Use convention-based paired fields: {Name}Choices
+ {Name}Chosen
type User struct {
ColorChoices []string // ["Red", "Blue", "Green"] - not rendered
ColorChosen int `vee:"type:'select',label:'Favorite Color'"` // renders as <select>
}
type User struct {
SkillChoices []string // ["Go", "JavaScript", "Python"]
SkillChosen []int `vee:"type:'select',multiple,label:'Skills'"` // multi-select
InterestChoices []string
InterestChosen []int `vee:"type:'checkbox',label:'Interests'"` // checkbox group
}
Select Dropdown (default):
ColorChosen int `vee:"type:'select'"` // Single select dropdown
SkillChosen []int `vee:"type:'select',multiple"` // Multi-select dropdown
Radio Button Group:
SizeChosen int `vee:"type:'radio'"` // Radio buttons (single-select only)
Checkbox Group:
FeatureChosen []int `vee:"type:'checkbox'"` // Checkbox group (multi-select only)
vee enforces strict conventions for multi-value fields:
- Paired fields required: Every
{Name}Choices
must have a corresponding{Name}Chosen
- Choices field type: Must be
[]string
or slice of any type implementingString()
- Chosen field type: Must be
int
(single-select) or[]int
(multi-select) - Index validation: All chosen indices must be within range of available choices
- Non-empty choices: Choices slice cannot be empty
- Form binding validation: Invalid form indices return binding errors
Validation Errors:
// ❌ Missing Chosen field
type User struct {
ColorChoices []string // Error: requires ColorChosen
}
// ❌ Wrong Chosen type
type User struct {
ColorChoices []string
ColorChosen string // Error: must be int or []int
}
// ❌ Index out of range (during rendering)
user := User{
ColorChoices: []string{"Red", "Blue"},
ColorChosen: 5, // Error: index 5 out of range for 2 choices
}
// ❌ Invalid form data (during binding)
formData := map[string][]string{
"color_chosen": {"5"}, // Error: index 5 out of range for 2 choices
// or
"color_chosen": {"invalid"}, // Error: invalid index 'invalid' for field 'color_chosen'
}
Choices can be any type implementing String()
method:
type Status int
func (s Status) String() string { return "..." }
type User struct {
StatusChoices []Status
StatusChosen int `vee:"type:'select',label:'Status'"`
}
Default Behavior: vee automatically generates <label>
elements for all form fields to improve accessibility and usability.
Labels are generated using this priority order:
- Custom label: Use
label:'Custom Text'
attribute if specified - Human-readable field name: Convert field name from CamelCase to spaced text
type User struct {
Name string // Label: "Name"
FirstName string // Label: "First Name"
EmailAddress string // Label: "Email Address"
IsActive bool // Label: "Is Active"
}
Labels are properly associated with inputs using the for
attribute:
<label for="field_id">Field Label</label>
<input type="text" name="field_name" id="field_id" ...>
Custom Label Text:
Name string `vee:"label:'Full Name'"`
Skip Label Generation:
Password string `vee:"type:'password',nolabel"`
- Select dropdowns: Get a standard
<label>
element - Radio/checkbox groups: Wrapped in
<fieldset><legend>
for semantic grouping
type Form struct {
ColorChoices []string
ColorChosen int `vee:"type:'select',label:'Favorite Color'"` // <label>
SizeChoices []string
SizeChosen int `vee:"type:'radio',label:'Size'"` // <fieldset><legend>
}
Default Behavior: All public struct fields are processed automatically. You don't need to add vee
tags unless you want to customize field behavior.
type User struct {
Name string // Processed with auto-derived name "name", label "Name"
Email string // Processed with auto-derived name "email", label "Email"
Age int // Processed with auto-derived name "age", label "Age"
}
Use vee:"-"
to skip fields during rendering and binding:
type User struct {
Name string // Processed (no tag needed)
Email string `vee:"type:'email'"` // Processed with custom type
Internal string `vee:"-"` // Skipped
}
type User struct {
Name string `vee:"required,label:'Full Name'" css:"border rounded px-3 py-2"`
Email string `vee:"$userEmail,type:'email',required" css:"w-full"`
Age int `vee:"min:18,max:120"`
Bio *string `vee:"placeholder:'Tell us about yourself'" css:"h-24"`
Website *string `vee:"type:'url'"` // Optional URL field
Score *float64 `vee:"min:0,max:100"` // Optional score field
Active bool `vee:"label:'Account Active'"`
Birthday time.Time `vee:"type:'date'"`
ColorChoices []string // ["Red", "Blue", "Green"]
ColorChosen int `vee:"type:'select',label:'Favorite Color'"`
}
// Render HTML form
html, err := vee.Render(User{
ColorChoices: []string{"Red", "Blue", "Green"},
ColorChosen: 1, // "Blue" selected
})
// Bind form data
var user User
err := vee.Bind(r, &user)
- Field Processing: All public struct fields are processed by default - no
vee
tags required unless customizing behavior - Framework agnostic - works with any
http.Request
- No nested struct support
- Limited type support for simplicity:
string
,int
,int64
,float64
,bool
,time.Time
,time.Duration
and their pointer equivalents - Pointer support: All base types support pointer variants (
*string
,*int
, etc.) - Pointer rendering: Nil pointers render with empty/zero values, non-nil render with actual values
- Pointer binding: Form data presence creates new pointer with parsed value, absence leaves field nil
- Multi-value support: Choices/Chosen convention for select dropdowns, radio groups, and checkbox groups
- Multi-value validation: Strict validation of field pairs, types, and index ranges
- Form data binding uses built-in
strconv
package for type conversion - Invalid numeric values are silently ignored (fields remain unchanged)
- Boolean checkbox binding: Presence in form data sets field to
true
, absence sets tofalse
(standard checkbox behavior) - Time field binding: Supports
date
(2006-01-02),time
(15:04), anddatetime-local
(2006-01-02T15:04) formats - Duration field binding: Converts between numeric input and
time.Duration
using configurable units (ms/s/m/h), defaults to seconds