Tailwind CSS and Alpine JS Counter
Counters are a great way to display numbers in a visually appealing format. They allow you to select a specific number using buttons.
This component requires Alpine JS v3 to function properly. Some advanced features may require additional Alpine plugins (such as focus).
Tell Me MoreDefault counter
A counter with two buttons to increase or decrease the number and an input field to display the number.
Classic vs Modern Code Style
The difference between the two versions is how they're written. The classic version uses older-style classes like 'text-red-500' for styling, while the modern version, uses CSS variables and semantic names like 'text-primary' for theming. It's important to note that 'Classic' doesn't mean an older version—they both use Tailwind V4. Tell me more.
<div x-data="{ currentVal: 1, minVal: 0, maxVal: 10, decimalPoints: 0, incrementAmount: 1 }" class="">
<label for="counterInput" class="">Items(s)</label>
<div x-on:dblclick.prevent class="">
<button x-on:click="currentVal = Math.max(minVal, currentVal - incrementAmount)" class="" aria-label="subtract">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15"/>
</svg>
</button>
<input x-model="currentVal.toFixed(decimalPoints)" id="counterInput" type="text" class="" readonly />
<button x-on:click="currentVal = Math.min(maxVal, currentVal + incrementAmount)" class="" aria-label="add">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</button>
</div>
</div>
<div x-data="{ currentVal: 1, minVal: 0, maxVal: 10, decimalPoints: 0, incrementAmount: 1 }" class="">
<label for="counterInput" class="">Items(s)</label>
<div x-on:dblclick.prevent class="">
<button x-on:click="currentVal = Math.max(minVal, currentVal - incrementAmount)" class="" aria-label="subtract">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15"/>
</svg>
</button>
<input x-model="currentVal.toFixed(decimalPoints)" id="counterInput" type="text" class="" readonly />
<button x-on:click="currentVal = Math.min(maxVal, currentVal + incrementAmount)" class="" aria-label="add">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</button>
</div>
</div>
@theme {
/* light theme */
--color-surface: var(--color-);
--color-surface-alt: var(--color-);
--color-on-surface: var(--color-);
--color-on-surface-strong: var(--color-);
--color-primary: var(--color-);
--color-on-primary: var(--color-);
--color-secondary: var(--color-);
--color-on-secondary: var(--color-);
--color-outline: ;
--color-outline-strong: var(--color-);
/* dark theme */
--color-surface-dark: var(--color-);
--color-surface-dark-alt: var(--color-);
--color-on-surface-dark: var(--color-);
--color-on-surface-dark-strong: var(--color-);
--color-primary-dark: var(--color-);
--color-on-primary-dark: var(--color-);
--color-secondary-dark: var(--color-);
--color-on-secondary-dark: var(--color-);
--color-outline-dark: var(--color-);
--color-outline-dark-strong: var(--color-);
/* shared colors */
--color-info: var(--color-);
--color-on-info: var(--color-);
--color-success: var(--color-);
--color-on-success: var(--color-);
--color-warning: var(--color-);
--color-on-warning: var(--color-);
--color-danger: var(--color-);
--color-on-danger: var(--color-);
/* border radius */
--radius-radius: var(--radius);
}
Split counter
A counter with two buttons to increase or decrease the number and an input field to display the number.
Classic vs Modern Code Style
The difference between the two versions is how they're written. The classic version uses older-style classes like 'text-red-500' for styling, while the modern version, uses CSS variables and semantic names like 'text-primary' for theming. It's important to note that 'Classic' doesn't mean an older version—they both use Tailwind V4. Tell me more.
<div x-data="{ currentVal: 1, minVal: 0, maxVal: 10, decimalPoints: 0, incrementAmount: 1 }" class="">
<label for="counterInput" class="">Items(s)</label>
<div x-on:dblclick.prevent class="">
<button x-on:click="currentVal = Math.max(minVal, currentVal - incrementAmount)" class="" aria-label="subtract">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15"/>
</svg>
</button>
<input x-model="currentVal.toFixed(decimalPoints)" id="counterInput" type="text" class="" readonly />
<button x-on:click="currentVal = Math.min(maxVal, currentVal + incrementAmount)" class="" aria-label="add">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</button>
</div>
</div>
<div x-data="{ currentVal: 1, minVal: 0, maxVal: 10, decimalPoints: 0, incrementAmount: 1 }" class="">
<label for="counterInput" class="">Items(s)</label>
<div x-on:dblclick.prevent class="">
<button x-on:click="currentVal = Math.max(minVal, currentVal - incrementAmount)" class="" aria-label="subtract">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15"/>
</svg>
</button>
<input x-model="currentVal.toFixed(decimalPoints)" id="counterInput" type="text" class="" readonly />
<button x-on:click="currentVal = Math.min(maxVal, currentVal + incrementAmount)" class="" aria-label="add">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" stroke="currentColor" fill="none" stroke-width="2" class="size-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
</svg>
</button>
</div>
</div>
@theme {
/* light theme */
--color-surface: var(--color-);
--color-surface-alt: var(--color-);
--color-on-surface: var(--color-);
--color-on-surface-strong: var(--color-);
--color-primary: var(--color-);
--color-on-primary: var(--color-);
--color-secondary: var(--color-);
--color-on-secondary: var(--color-);
--color-outline: ;
--color-outline-strong: var(--color-);
/* dark theme */
--color-surface-dark: var(--color-);
--color-surface-dark-alt: var(--color-);
--color-on-surface-dark: var(--color-);
--color-on-surface-dark-strong: var(--color-);
--color-primary-dark: var(--color-);
--color-on-primary-dark: var(--color-);
--color-secondary-dark: var(--color-);
--color-on-secondary-dark: var(--color-);
--color-outline-dark: var(--color-);
--color-outline-dark-strong: var(--color-);
/* shared colors */
--color-info: var(--color-);
--color-on-info: var(--color-);
--color-success: var(--color-);
--color-on-success: var(--color-);
--color-warning: var(--color-);
--color-on-warning: var(--color-);
--color-danger: var(--color-);
--color-on-danger: var(--color-);
/* border radius */
--radius-radius: var(--radius);
}
Data
List of all Alpine JS data used in this component.
Property | Description |
---|---|
currentVal |
Number - Current value of the input |
minVal |
Number - Minimum acceptable value for the input |
maxVal |
Number - Maximum acceptable value for the input |
decimalPoints |
Number - Number of decimal points |
incrementAmount |
Number - Number of steps with each increment |
Keyboard Navigation
Key | Action |
---|---|
Tab |
Next focusable element gets the focus |
Space | Focused item gets selected |