This post is more than a year old. There is a chance the content is outdated.

Building a Constrained Textarea using Alpine.js

Building a Constrained Textarea using Alpine.js

I had the need for a constrained textarea on a recent project. It's basically just a normal text area that counts down how many characters you have left to type in before it cuts you off.

I could have made it with jQuery or used a jQuery plugin which would have worked just fine, but I don't load jQuery on newer projects, so I made it using Alpine.js and extracted it to a blade component for reuse. The use of the blade components $attributes feature allows us to use it as a Livewire component with no extra steps.

There's really not too much to this one, so here are the steps we'll take.

  1. Create an Alpine.js component that wraps a textarea.
  2. Add a limit to the textarea.
  3. Calculate the amount of characters remaining.
  4. Display the amount of characters remaining.

1. Create an Alpine.js Component

So first we need to have a component with a textarea inside to work with. Which is fairly straightforward:

In Alpine.js, the existence of x-data tells the library to render this element as an Alpine component.

1<div x-data>
2 <textarea></textarea>

Then we need to give it a place to hold the content, and bind that value to the textarea:

1@props(['value' => ''])
4 x-data="{
5 content: '{{ $value }}',
6 }"
8 <textarea
9 x-model="content"
10 ></textarea>

As you can see we created a prop for value, and pass that into an Alpine property called content. We then added an x-model to the textarea so when the value is set, or the user types into the textarea the content property will be updated.

2. Add a limit to the text area

To calculate the number of characters remaining, we need to define a maximum number of characters we allow, or limit.

We will add this as both a property of the Blade component, as well as a property of the Alpine component so that we can pass it in.

1@props(['limit' => 255, 'value' => ''])
4 x-data="{
5 content: '{{ $value }}',
6 limit: {{ $limit }},
7 }"
9 <textarea
10 x-model="content"
11 maxlength="{{ $limit }}"
12 ></textarea>

We accept the limit as a prop to the Blade component, we also added a limit property to the Alpine component and pass it the value of the property from the Blade component.

Then we add a maxlength to the textarea using the same value to stop the person from typing when they run out of characters.

3. Calculate the number of characters remaining

Now we need to take the input as the user types it in, and compare it to the maximum characters we are allowed to have. We can do this with a getter in Javascript which will bind a property to a function that will be called when that property is looked up.

1get remaining() {
2 return this.limit - this.content.length

Here we return the maximum characters allowed minus the current characters typed in.

4. Display the amount of characters remaining

Finally, we will display how many characters are left to the user by fetching the above property:

2 <small>You have <span x-text="remaining"></span> characters remaining.</small>

We bind the remaining property to the x-text value of this span which will be updated every time the user types into the textarea due to the binded x-model.

For bonus points, we can utilize Blade's $attributes property to insert any other data passed through to the component, such as Livewire properties.

2 x-model="content"
3 maxlength="{{ $limit }}"
4 {{ $attributes }}

This is the final component:


1@props(['limit' => 255, 'value' => ''])
4 x-data="{
5 content: '{{ $value }}',
6 limit: {{ $limit }},
7 get remaining() {
8 return this.limit - this.content.length
9 }
10 }"
12 <textarea
13 x-model="content"
14 maxlength="{{ $limit }}"
15 {{ $attributes }}
16 ></textarea>
18 <p>
19 <small>You have <span x-text="remaining"></span> characters remaining.</small>
20 </p>

This is how you would use it:

2 limit="255"
3 wire:model.defer="description"
4 :value="$item->description"
5 placeholder="{{ __('Description') }}"
6 class="border-gray-300 focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 rounded-md shadow-sm mt-1 block w-full"

This is what it would look like:

I'm sure there are more feature and props you can add to make it even better, but this is a good starting point! There are also probably 10 ways to get this exact functionality, but this is what I came up with.

Anthony Rappa

By Anthony Rappa

Hello! I'm a full stack developer from Long Island, New York. Working mainly with Laravel, Tailwind, Livewire, and Alpine.js (TALL Stack). I share everything I know about these tools and more, as well as any useful resources I find from the community. You can find me on GitHub and LinkedIn.