Input Fields
SimpleText, email, password, and textarea inputs with validation states and interactive features
Live Demo
Basic Text Input
Simple text input with validation and required field handling
Try it: Leave the field empty and click outside to see validation in action.
Resize to see how the component adapts to different screen sizes
📖 Basic Text Input Overview
Simple text input with validation and required field handling
💡 Key Features
- • Basic Text Input functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with input capabilities
- • Enhanced with text capabilities
- • Enhanced with email capabilities
- • Enhanced with password capabilities
- • Enhanced with textarea capabilities
- • Enhanced with validation capabilities
- • Enhanced with form capabilities
🏗️ Basic Text Input HTML Structure
This basic text input component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-inputs-basic"
Connects this HTML element to the Basic Text Input Stimulus controller
data-action="click->forms-inputs-basic#method"
Defines click events that trigger basic text input controller methods
data-forms-inputs-basic-target="element"
Identifies elements that the Basic Text Input controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance basic text input accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Basic Text Input Tailwind Classes
This basic text input component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for basic text input contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Basic Text Input Design
- •
sm:
- Small screens (640px+) optimized for basic text input - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures basic text input works on all devices
⚡ Basic Text Input JavaScript Logic
The Basic Text Input component uses a dedicated Stimulus controller (forms-inputs-basic
) to handle basic text input interactions and manage component state.
🎯 Basic Text Input Controller Features
Targets
DOM elements the basic text input controller can reference and manipulate
Values
Configuration data for basic text input behavior passed from HTML
Actions
Methods triggered by basic text input user events and interactions
Lifecycle
Setup and cleanup methods for basic text input initialization
🔄 Basic Text Input Event Flow
- 1. User interacts with basic text input element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Basic Text Input controller method executes with access to targets and values
- 4. Controller updates DOM with new basic text input state or visual changes
- 5. CSS transitions provide smooth visual feedback for basic text input interactions
<div class="max-w-md mx-auto">
<div data-controller="inputs-basic" data-inputs-basic-required-value="true">
<label for="basic-name" class="block text-sm font-medium text-green-900 mb-2">
Full Name <span class="text-red-500">*</span>
</label>
<input
data-inputs-basic-target="field"
type="text"
id="basic-name"
name="full_name"
placeholder="Enter your full name"
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
>
<p data-inputs-basic-target="error" class="mt-1 text-sm text-red-600 hidden"></p>
</div>
<div class="mt-4 p-3 bg-green-50 rounded-lg">
<p class="text-sm text-green-700">
<strong>Try it:</strong> Leave the field empty and click outside to see validation in action.
</p>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["field", "error"]
static values = {
required: Boolean,
pattern: String
}
connect() {
this.validateOnInput = this.validateOnInput.bind(this)
this.validateOnBlur = this.validateOnBlur.bind(this)
if (this.hasFieldTarget) {
this.fieldTarget.addEventListener("input", this.validateOnInput)
this.fieldTarget.addEventListener("blur", this.validateOnBlur)
this.fieldTarget.addEventListener("focus", this.clearError.bind(this))
}
}
disconnect() {
if (this.hasFieldTarget) {
this.fieldTarget.removeEventListener("input", this.validateOnInput)
this.fieldTarget.removeEventListener("blur", this.validateOnBlur)
}
}
validateOnInput(event) {
// Clear error state when user starts typing
if (this.hasErrorTarget && this.errorTarget.textContent) {
this.clearError()
}
}
validateOnBlur(event) {
const value = event.target.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
if (value && this.patternValue) {
return this.validatePattern(value)
}
return true
}
validatePattern(value) {
const regex = new RegExp(this.patternValue)
const isValid = regex.test(value)
if (!isValid) {
this.showError("Please enter a valid format")
} else {
this.clearError()
}
return isValid
}
showError(message) {
if (this.hasErrorTarget) {
this.errorTarget.textContent = message
this.errorTarget.classList.remove("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.add("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.remove("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
clearError() {
if (this.hasErrorTarget) {
this.errorTarget.textContent = ""
this.errorTarget.classList.add("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.remove("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.add("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
// Public method to validate the form field
validate() {
if (!this.hasFieldTarget) return true
const value = this.fieldTarget.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
if (value && this.patternValue) {
return this.validatePattern(value)
}
this.clearError()
return true
}
}
Email Input with Icon
Email input with validation, icon, and error states
Try it: Enter an invalid email format and watch the icon color change along with validation feedback.
Resize to see how the component adapts to different screen sizes
📖 Email Input with Icon Overview
Email input with validation, icon, and error states
💡 Key Features
- • Email Input with Icon functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with input capabilities
- • Enhanced with text capabilities
- • Enhanced with email capabilities
- • Enhanced with password capabilities
- • Enhanced with textarea capabilities
- • Enhanced with validation capabilities
- • Enhanced with form capabilities
🏗️ Email Input with Icon HTML Structure
This email input with icon component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-inputs-email"
Connects this HTML element to the Email Input with Icon Stimulus controller
data-action="click->forms-inputs-email#method"
Defines click events that trigger email input with icon controller methods
data-forms-inputs-email-target="element"
Identifies elements that the Email Input with Icon controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance email input with icon accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Email Input with Icon Tailwind Classes
This email input with icon component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for email input with icon contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Email Input with Icon Design
- •
sm:
- Small screens (640px+) optimized for email input with icon - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures email input with icon works on all devices
⚡ Email Input with Icon JavaScript Logic
The Email Input with Icon component uses a dedicated Stimulus controller (forms-inputs-email
) to handle email input with icon interactions and manage component state.
🎯 Email Input with Icon Controller Features
Targets
DOM elements the email input with icon controller can reference and manipulate
Values
Configuration data for email input with icon behavior passed from HTML
Actions
Methods triggered by email input with icon user events and interactions
Lifecycle
Setup and cleanup methods for email input with icon initialization
🔄 Email Input with Icon Event Flow
- 1. User interacts with email input with icon element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Email Input with Icon controller method executes with access to targets and values
- 4. Controller updates DOM with new email input with icon state or visual changes
- 5. CSS transitions provide smooth visual feedback for email input with icon interactions
<div class="max-w-md mx-auto">
<div data-controller="inputs-email" data-inputs-email-required-value="true">
<label for="email-input" class="block text-sm font-medium text-green-900 mb-2">
Email Address <span class="text-red-500">*</span>
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg data-inputs-email-target="icon" class="h-5 w-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207"></path>
</svg>
</div>
<input
data-inputs-email-target="field"
type="email"
id="email-input"
name="email"
placeholder="[email protected]"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
>
</div>
<p data-inputs-email-target="error" class="mt-1 text-sm text-red-600 hidden"></p>
</div>
<div class="mt-4 p-3 bg-green-50 rounded-lg">
<p class="text-sm text-green-700">
<strong>Try it:</strong> Enter an invalid email format and watch the icon color change along with validation feedback.
</p>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["field", "error", "icon"]
static values = {
required: Boolean
}
connect() {
this.validateOnInput = this.validateOnInput.bind(this)
this.validateOnBlur = this.validateOnBlur.bind(this)
if (this.hasFieldTarget) {
this.fieldTarget.addEventListener("input", this.validateOnInput)
this.fieldTarget.addEventListener("blur", this.validateOnBlur)
this.fieldTarget.addEventListener("focus", this.clearError.bind(this))
}
}
disconnect() {
if (this.hasFieldTarget) {
this.fieldTarget.removeEventListener("input", this.validateOnInput)
this.fieldTarget.removeEventListener("blur", this.validateOnBlur)
}
}
validateOnInput(event) {
const value = event.target.value
// Clear error state when user starts typing
if (this.hasErrorTarget && this.errorTarget.textContent) {
this.clearError()
}
// Real-time validation feedback (without showing error immediately)
if (value.length > 0) {
this.validateEmail(value, false)
}
}
validateOnBlur(event) {
const value = event.target.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
if (value) {
return this.validateEmail(value, true)
}
return true
}
validateEmail(value, showError = true) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const isValid = emailRegex.test(value)
if (!isValid && showError) {
this.showError("Please enter a valid email address")
} else if (isValid) {
this.clearError()
}
return isValid
}
showError(message) {
if (this.hasErrorTarget) {
this.errorTarget.textContent = message
this.errorTarget.classList.remove("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.add("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.remove("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
if (this.hasIconTarget) {
this.iconTarget.classList.add("text-red-500")
this.iconTarget.classList.remove("text-gray-400")
}
}
clearError() {
if (this.hasErrorTarget) {
this.errorTarget.textContent = ""
this.errorTarget.classList.add("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.remove("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.add("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
if (this.hasIconTarget) {
this.iconTarget.classList.remove("text-red-500")
this.iconTarget.classList.add("text-gray-400")
}
}
// Public method to validate the form field
validate() {
if (!this.hasFieldTarget) return true
const value = this.fieldTarget.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
if (value) {
return this.validateEmail(value, true)
}
this.clearError()
return true
}
}
Password Input with Toggle
Password field with show/hide toggle functionality
Try it: Click the eye icon to toggle password visibility. Enter less than 6 characters to see validation.
Resize to see how the component adapts to different screen sizes
📖 Password Input with Toggle Overview
Password field with show/hide toggle functionality
💡 Key Features
- • Password Input with Toggle functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with input capabilities
- • Enhanced with text capabilities
- • Enhanced with email capabilities
- • Enhanced with password capabilities
- • Enhanced with textarea capabilities
- • Enhanced with validation capabilities
- • Enhanced with form capabilities
🏗️ Password Input with Toggle HTML Structure
This password input with toggle component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-inputs-password"
Connects this HTML element to the Password Input with Toggle Stimulus controller
data-action="click->forms-inputs-password#method"
Defines click events that trigger password input with toggle controller methods
data-forms-inputs-password-target="element"
Identifies elements that the Password Input with Toggle controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance password input with toggle accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Password Input with Toggle Tailwind Classes
This password input with toggle component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for password input with toggle contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Password Input with Toggle Design
- •
sm:
- Small screens (640px+) optimized for password input with toggle - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures password input with toggle works on all devices
⚡ Password Input with Toggle JavaScript Logic
The Password Input with Toggle component uses a dedicated Stimulus controller (forms-inputs-password
) to handle password input with toggle interactions and manage component state.
🎯 Password Input with Toggle Controller Features
Targets
DOM elements the password input with toggle controller can reference and manipulate
Values
Configuration data for password input with toggle behavior passed from HTML
Actions
Methods triggered by password input with toggle user events and interactions
Lifecycle
Setup and cleanup methods for password input with toggle initialization
🔄 Password Input with Toggle Event Flow
- 1. User interacts with password input with toggle element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Password Input with Toggle controller method executes with access to targets and values
- 4. Controller updates DOM with new password input with toggle state or visual changes
- 5. CSS transitions provide smooth visual feedback for password input with toggle interactions
<div class="max-w-md mx-auto">
<div data-controller="inputs-password" data-inputs-password-required-value="true">
<label for="password-input" class="block text-sm font-medium text-green-900 mb-2">
Password <span class="text-red-500">*</span>
</label>
<div class="relative">
<input
data-inputs-password-target="field"
type="password"
id="password-input"
name="password"
placeholder="Enter your password"
class="block w-full pr-10 px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm"
>
<button
type="button"
data-action="click->inputs-password#toggle"
class="absolute inset-y-0 right-0 pr-3 flex items-center"
title="Toggle password visibility"
>
<svg data-inputs-password-target="showIcon" class="h-5 w-5 text-gray-400 hover:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
<svg data-inputs-password-target="hideIcon" class="h-5 w-5 text-gray-400 hover:text-gray-600 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L3 3m6.878 6.878L21 21"></path>
</svg>
</button>
</div>
<p data-inputs-password-target="error" class="mt-1 text-sm text-red-600 hidden"></p>
</div>
<div class="mt-4 p-3 bg-green-50 rounded-lg">
<p class="text-sm text-green-700">
<strong>Try it:</strong> Click the eye icon to toggle password visibility. Enter less than 6 characters to see validation.
</p>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["field", "error", "showIcon", "hideIcon"]
static values = {
required: Boolean
}
connect() {
this.validateOnInput = this.validateOnInput.bind(this)
this.validateOnBlur = this.validateOnBlur.bind(this)
if (this.hasFieldTarget) {
this.fieldTarget.addEventListener("input", this.validateOnInput)
this.fieldTarget.addEventListener("blur", this.validateOnBlur)
this.fieldTarget.addEventListener("focus", this.clearError.bind(this))
}
}
disconnect() {
if (this.hasFieldTarget) {
this.fieldTarget.removeEventListener("input", this.validateOnInput)
this.fieldTarget.removeEventListener("blur", this.validateOnBlur)
}
}
validateOnInput(event) {
// Clear error state when user starts typing
if (this.hasErrorTarget && this.errorTarget.textContent) {
this.clearError()
}
}
validateOnBlur(event) {
const value = event.target.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
// Basic password strength feedback
if (value && value.length < 6) {
this.showError("Password should be at least 6 characters long")
return false
}
return true
}
toggle() {
const isPassword = this.fieldTarget.type === "password"
this.fieldTarget.type = isPassword ? "text" : "password"
if (this.hasShowIconTarget && this.hasHideIconTarget) {
if (isPassword) {
this.showIconTarget.classList.add("hidden")
this.hideIconTarget.classList.remove("hidden")
} else {
this.showIconTarget.classList.remove("hidden")
this.hideIconTarget.classList.add("hidden")
}
}
}
showError(message) {
if (this.hasErrorTarget) {
this.errorTarget.textContent = message
this.errorTarget.classList.remove("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.add("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.remove("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
clearError() {
if (this.hasErrorTarget) {
this.errorTarget.textContent = ""
this.errorTarget.classList.add("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.remove("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.add("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
// Public method to validate the form field
validate() {
if (!this.hasFieldTarget) return true
const value = this.fieldTarget.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
if (value && value.length < 6) {
this.showError("Password should be at least 6 characters long")
return false
}
this.clearError()
return true
}
}
Textarea with Counter
Textarea with character counter and length validation
0/100
Try it: Start typing to see the character counter update. Notice how the color changes as you approach the limit.
Resize to see how the component adapts to different screen sizes
📖 Textarea with Counter Overview
Textarea with character counter and length validation
💡 Key Features
- • Textarea with Counter functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with input capabilities
- • Enhanced with text capabilities
- • Enhanced with email capabilities
- • Enhanced with password capabilities
- • Enhanced with textarea capabilities
- • Enhanced with validation capabilities
- • Enhanced with form capabilities
🏗️ Textarea with Counter HTML Structure
This textarea with counter component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-inputs-textarea"
Connects this HTML element to the Textarea with Counter Stimulus controller
data-action="click->forms-inputs-textarea#method"
Defines click events that trigger textarea with counter controller methods
data-forms-inputs-textarea-target="element"
Identifies elements that the Textarea with Counter controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance textarea with counter accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Textarea with Counter Tailwind Classes
This textarea with counter component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for textarea with counter contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Textarea with Counter Design
- •
sm:
- Small screens (640px+) optimized for textarea with counter - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures textarea with counter works on all devices
⚡ Textarea with Counter JavaScript Logic
The Textarea with Counter component uses a dedicated Stimulus controller (forms-inputs-textarea
) to handle textarea with counter interactions and manage component state.
🎯 Textarea with Counter Controller Features
Targets
DOM elements the textarea with counter controller can reference and manipulate
Values
Configuration data for textarea with counter behavior passed from HTML
Actions
Methods triggered by textarea with counter user events and interactions
Lifecycle
Setup and cleanup methods for textarea with counter initialization
🔄 Textarea with Counter Event Flow
- 1. User interacts with textarea with counter element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Textarea with Counter controller method executes with access to targets and values
- 4. Controller updates DOM with new textarea with counter state or visual changes
- 5. CSS transitions provide smooth visual feedback for textarea with counter interactions
<div class="max-w-md mx-auto">
<div data-controller="inputs-textarea" data-inputs-textarea-max-length-value="100">
<label for="bio-textarea" class="block text-sm font-medium text-green-900 mb-2">
Bio
</label>
<textarea
data-inputs-textarea-target="field"
id="bio-textarea"
name="bio"
rows="3"
maxlength="100"
placeholder="Tell us about yourself..."
class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-green-500 focus:border-green-500 sm:text-sm resize-none"
></textarea>
<div class="mt-1 flex justify-between">
<p data-inputs-textarea-target="error" class="text-sm text-red-600 hidden"></p>
<p data-inputs-textarea-target="counter" class="text-sm text-gray-500">0/100</p>
</div>
</div>
<div class="mt-4 p-3 bg-green-50 rounded-lg">
<p class="text-sm text-green-700">
<strong>Try it:</strong> Start typing to see the character counter update. Notice how the color changes as you approach the limit.
</p>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["field", "error", "counter"]
static values = {
maxLength: Number,
required: Boolean
}
connect() {
this.validateOnInput = this.validateOnInput.bind(this)
this.validateOnBlur = this.validateOnBlur.bind(this)
if (this.hasFieldTarget) {
this.fieldTarget.addEventListener("input", this.validateOnInput)
this.fieldTarget.addEventListener("blur", this.validateOnBlur)
this.fieldTarget.addEventListener("focus", this.clearError.bind(this))
// Initialize character counter if present
if (this.hasCounterTarget && this.maxLengthValue) {
this.updateCounter()
}
}
}
disconnect() {
if (this.hasFieldTarget) {
this.fieldTarget.removeEventListener("input", this.validateOnInput)
this.fieldTarget.removeEventListener("blur", this.validateOnBlur)
}
}
validateOnInput(event) {
// Update character counter
if (this.hasCounterTarget && this.maxLengthValue) {
this.updateCounter()
}
// Clear error state when user starts typing
if (this.hasErrorTarget && this.errorTarget.textContent) {
this.clearError()
}
}
validateOnBlur(event) {
const value = event.target.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
return true
}
updateCounter() {
if (this.hasCounterTarget && this.hasFieldTarget) {
const current = this.fieldTarget.value.length
const max = this.maxLengthValue
this.counterTarget.textContent = `${current}/${max}`
// Change color when approaching limit
if (current > max * 0.8) {
this.counterTarget.classList.add("text-amber-600")
this.counterTarget.classList.remove("text-gray-500")
} else {
this.counterTarget.classList.remove("text-amber-600")
this.counterTarget.classList.add("text-gray-500")
}
// Show warning when near or at limit
if (current >= max) {
this.counterTarget.classList.add("text-red-500")
this.counterTarget.classList.remove("text-amber-600", "text-gray-500")
}
}
}
showError(message) {
if (this.hasErrorTarget) {
this.errorTarget.textContent = message
this.errorTarget.classList.remove("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.add("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.remove("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
clearError() {
if (this.hasErrorTarget) {
this.errorTarget.textContent = ""
this.errorTarget.classList.add("hidden")
}
if (this.hasFieldTarget) {
this.fieldTarget.classList.remove("border-red-500", "focus:border-red-500", "focus:ring-red-500")
this.fieldTarget.classList.add("border-gray-300", "focus:border-green-500", "focus:ring-green-500")
}
}
// Public method to validate the form field
validate() {
if (!this.hasFieldTarget) return true
const value = this.fieldTarget.value.trim()
if (this.requiredValue && !value) {
this.showError("This field is required")
return false
}
this.clearError()
return true
}
}
Installation & Usage
Copy the ERB template
Copy the ERB code from the template tab above and paste it into your Rails view file.
Add the Stimulus controller
Create the Stimulus controller file in your JavaScript controllers directory.