Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Filament Character Counter Laravel Package

schmeits/filament-character-counter

Easy drop-in Filament character counter for TextField, Textarea and RichEditor fields in Filament v4 and v5. Add a characterLimit or maxLength to display a live character count, optionally shown inside the control. Translation strings are publishable, and v3.2 is supported via 1.x.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Match the version to your stack before anything else. The version matrix is the single most important install decision:

Filament Package PHP
3.2 ^1.0 > 8.1
4.x ^5.0 > 8.2
5.x ^5.0 > 8.2
composer require schmeits/filament-character-counter:"^5.0"

Pin the major explicitly — a bare composer require on a Filament 3.2 app will happily drag in a 5.x that requires filament/forms 4+/5+ and leave you resolving conflicts for an afternoon. If you're on 3.2, you also lose RichEditor support (added in 1.3.0) and showInsideControl on Textarea (fixed in 1.3.3).

2. Publish translations only if you need to override the copy. The package ships translations out of the box; publishing is for rewording:

php artisan vendor:publish --tag="filament-character-counter-translations"
php artisan optimize:clear

Don't publish "just in case." Published lang files go stale silently (see Gotchas — character_seperator is no longer read as of v5.2.0).

3. Look at one existing field first. The fastest way to understand the package is to swap a single import and add one method call:

use Schmeits\FilamentCharacterCounter\Forms\Components\TextInput;

TextInput::make('title')
    ->characterLimit(60),   // shows "42 / 60 characters"

That's the whole API surface for 90% of use. Everything else — maxLength(), showInsideControl(), showCharacterCounter() — is a modifier on top.

4. Pick your first real use case: SEO metadata. This package's natural home is a "SEO" tab on a resource, where editors need to see length against Google's truncation points:

TextInput::make('meta_title')
    ->characterLimit(60)
    ->maxLength(70),

Textarea::make('meta_description')
    ->characterLimit(155)
    ->maxLength(170)
    ->rows(3),

Notice both methods used together — that pattern is explained in Implementation Patterns and is the correct default.


Implementation Patterns

Prefer maxLength() alone for hard limits, characterLimit() for guidance

The two methods have distinct semantics, and the distinction has changed across releases:

  • maxLength(N) — hard enforcement (HTML maxlength + Filament's validation) and renders the counter. No need to add characterLimit() alongside it.
  • characterLimit(N) — soft limit since v5.0.1: no maxlength, no validation. As of v5.2.0 an overflow is rendered in words, e.g. (12 too many), bolded and announced to screen readers.

So the correct daily-use patterns are:

// "You must not exceed this" — API field, slug, external ID
TextInput::make('slug')->maxLength(100),

// "Aim for this, but don't break the page" — copy fields
Textarea::make('excerpt')->characterLimit(155)->maxLength(200),

The second pattern is what you want for marketing copy: the soft counter nudges ("12 too many"), the hard ceiling prevents a pathological paste from being stored. Don't rely on characterLimit() for data integrity — it does not stop the value reaching your database.

Alias imports when only some fields need counters

The package's components are Filament components, so importing them shadows the originals. In a form with twelve fields where three need counters, be explicit:

use Filament\Forms\Components\TextInput;
use Schmeits\FilamentCharacterCounter\Forms\Components\TextInput as CounterTextInput;
use Schmeits\FilamentCharacterCounter\Forms\Components\Textarea as CounterTextarea;

Blanket-swapping every import in a large resource makes it hard for the next developer to know which fields are counter-aware, and quietly couples unrelated fields to this dependency (which matters — see the maintenance note in Gotchas).

Wrap repeated limits in your own helper or enum

Limits are business rules that drift. Centralise them rather than sprinkling magic numbers:

enum FieldLimit: int
{
    case SeoTitle = 60;
    case SeoDescription = 155;
    case Excerpt = 200;
}
CounterTextInput::make('meta_title')
    ->characterLimit(FieldLimit::SeoTitle->value)
    ->maxLength(FieldLimit::SeoTitle->value + 10),

This keeps the validation rule and the counter reading from one source, which is the only way to avoid the classic "the counter said 60 but the validator says 65" bug.

Conditional counters via Closure + Get

The counter is dynamic-config aware, so you can gate it on sibling state:

use Filament\Forms\Get;

CounterTextarea::make('bio')
    ->characterLimit(255)
    ->showCharacterCounter(fn (Get $get) => $get('is_public')),

Practical uses: hide the counter on fields whose limit only applies to certain post types, on read-only contexts, or when a field is disabled anyway. Note the deprecation — use shouldShowCharacterCounter() if you need to read the resolved value; getShowCharacterCounter() is deprecated as of v5.2.0.

showInsideControl() for dense layouts

The counter renders as a helper below the field by default. Inside a Grid with columns(3) that produces a ragged, tall form. showInsideControl(true) puts it in the input's suffix area:

CounterTextInput::make('sku')
    ->showInsideControl()
    ->maxLength(20),

Use it on narrow grid columns and on RichEditor (v5.0.0 added proper spacing for the RichEditor counter, and v5.2.0 added .fi-fo-rich-editor-wrapper). Leave it off for textareas where the text can run to the full width — an inside counter fights the scrollbar.

RichEditor: count what the user sees, validate what you store

use Schmeits\FilamentCharacterCounter\Forms\Components\RichEditor;

RichEditor::make('content')
    ->characterLimit(2000)
    ->maxLength(2500),

The RichEditor counter reads the DOM text content of .fi-fo-rich-editor-content — i.e. visible text, not markup. That is the right behaviour for authors, but it means the browser count and the stored value diverge by the size of the HTML. Never validate the raw column with max:2000. Either strip tags before counting:

use Illuminate\Validation\Rule;

->rules([
    fn () => Rule::when(
        true,
        fn ($attribute, $value, $fail) => mb_strlen(strip_tags($value)) > 2000
            ? $fail('Content is too long.')
            : null,
    ),
])

…or store a denormalised content_length column on save via a model observer if you need to report on it in the admin. The first is simpler; the second is worth it if you expect to query on length later.

Treat translations as a theming layer

Override the label copy per panel or per tenant if your editors are non-technical:

// lang/vendor/filament-character-counter/en/character-counter.php (post-publish)
return [
    'character_label' => 'characters',
    'characters_over' => ':count too many',
];

characters_over is the v5.2.0 addition and is the one you'll actually want to tune — "too many" is blunter than most product copy.


Gotchas and Tips

characterLimit() will not protect your database

This bit people twice. v5.0.0 made characterLimit() call maxLength() under the hood; v5.0.1 reverted that ("now a soft limit again", issue #36). If you read a tutorial or a copy of the README from before v5.0.1, your fields are unenforced. Always confirm with a deliberate paste of 500 characters into a deployed form.

RichEditor toolbar features were silently disabled in 5.0.x–5.1.x

Through v5.1.0 the package rendered a copied Filament RichEditor view, which switched off file attachments, mentions, floating toolbars, link protocols and resizable images — no error, they just vanished. v5.2.0 fixed it by letting Filament render the editor and placing the counter around it. If any RichEditor in your app mysteriously lost its attachment button, this is why. Upgrade to ≥5.2.0 before you go debugging Filament's own config.

Unicode: emoji are one character, and Laravel agrees — but only since 5.2.0

The counter now counts per code point in the browser, matching Laravel's mb_strlen-based max: rule. On older versions an emoji counted as two in the browser and one in validation, producing an off-by-N mismatch that's miserable to reproduce. Same release fixed autofill, dictation, paste, $set() and form resets — pre-5.2.0 the counter only followed key presses. Test with a paste, not just typing.

Livewire ->live() and reactive updates

Counter stutter with ->live() was fixed back in 1.2.1, and v5.2.0 replaced the RichEditor's 500 ms polling loop (and its per-editor Livewire hook leak) with a commit hook. If you're on a version that polls, watch long editing sessions for memory growth — a leaked hook per editor mounts up in an SPA panel.

Publish-then-forget translations

character_seperator (note the spelling) was used in older versions and is no longer read as of v5.2.0. A stale published lang file that only overrides the separator will look like it's silently being ignored — because it is. After upgrading, diff your published file against the package's current source and delete keys you don't need to override.

Alpine and SPA navigation

Earlier versions copied rich-editor and textarea Blade views and had to work around async Alpine loading in SPA mode (1.3.2/1.3.3). These were real bugs with real fixes; if counters appear for a moment then vanish on wire:navigate, upgrade first. Debugging path: Vue/DevTools → check for x-data on the wrapper → console for Alpine expression errors → confirm the rendered markup contains fi-input-charcounter classes.

Styling: plain CSS, Filament variables, no build step

Since v5.2.0 the stylesheet is plain CSS using Filament's colour variables and works in dark mode with no compilation. The exceeded-state class is fi-input-charcounter-exceeded (previously warning) — update any custom CSS that targeted the old name. Because it's plain CSS with no build step, theming is a one-line override in your panel's custom stylesheet rather than a Vite dependency.

Extension point: apply the trait to your own fields

HasCharacterLimit is the trait that carries characterLimit(), maxLength() wiring and showCharacterCounter()/shouldShowCharacterCounter(). If you have a custom field extending Filament's TextInput (a currency input, a masked phone field), you don't need to swap it for the package's component — just use the trait:

use Schmeits\FilamentCharacterCounter\Forms\Concerns\HasCharacterLimit;

class CurrencyInput extends \Filament\Forms\Components\TextInput
{
    use HasCharacterLimit;
}

This is the cleanest integration point and the reason not to treat the components as an all-or-nothing replacement.

Dependency footprint and risk

Since v5.2.0 the package requires only filament/forms and filament/support rather than the whole filament/filament panel package — a real improvement, but also a sign that if you're pinned to an older Filament 4.x lockfile, Composer may resolve a different package version than you expect. Check composer why schmeits/filament-character-counter after upgrading.

With ~34 stars, zero dependents and a single maintainer, treat this as a small, forkable utility rather than core infrastructure. The mitigation is cheap: the surface is three components and a trait, the views are Blade you can vendor with vendor:publish, and the licence is MIT. If it ever stalls, replacing characterLimit() with a ->hint(fn ($state) => strlen($state).'/60') plus a maxLength() is a half-day of work — worth knowing before you spread it across fifty fields.

Testing your own forms

The package has unit tests for all three component types (composer test), so you can assert on rendered output in your own suite. For Livewire form tests, assert the maxlength attribute exists on hard-limited fields and that characterLimit-only fields are absent — that's the cheapest regression test for the soft-vs-hard confusion above.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core