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.
Architecture fit
This is a narrowly scoped, single-responsibility package: it contributes form-field components that extend Filament's own TextInput, Textarea and RichEditor to render a live character counter. It is not a plugin/panel provider in the usual Filament sense — there is no service provider registration in the README, no panel configuration, and no runtime hook into the request lifecycle beyond the form component itself.
HasCharacterLimit), three field subclasses, a handful of Blade views, a CSS file, and translation files.filament/forms and filament/support rather than filament/filament. That is the right call for a leaf package and means adoption does not pull a panel package into contexts that don't need it.Schmeits\FilamentCharacterCounter\Forms\Components\TextInput — i.e. developers swap their field imports. This is an import-level coupling, not a configuration-level one. If the package is later abandoned, removal is a mechanical find-and-replace, which keeps the exit cost low, but it does mean vendor-specific imports will proliferate across every resource that uses it unless you standardise on an internal wrapper.Integration feasibility
Feasible in an afternoon. composer require schmeits/filament-character-counter:"^5.2", optionally publish translations, then adopt per field.
The version matrix is the one point of confusion a TPM should resolve up front, because the README and the changelog disagree in emphasis:
| Package | Filament | PHP | Notes |
|---|---|---|---|
| 1.x | 3.2 | >8.1 | Legacy, out of scope |
| 4.x | 4.x | >8.2 | Superseded by 5.x |
| 5.x | 4.x – 5.x | >8.2 | Single line supports both majors |
The v5.2.0 notes add that Laravel 11 is no longer tested (Composer refuses those releases due to open advisories) and CI covers PHP 8.2–8.5 on Laravel 12 and 13. The README's PHP column has not been updated to reflect 8.3–8.5 coverage. Practically: standardise on ^5.2 and do not adopt the 4.x line.
Technical risk
Risk is low in absolute terms but non-zero, and clustered in three places:
$el.querySelector('.fi-fo-rich-editor-content'), a Filament-internal CSS class. This is a pragmatic and defensible choice (the TipTap instance isn't reliably reachable), but it is a private contract. A Filament minor/patch that renames or restructures that class silently degrades the counter — likely to "always zero" or "never updates" rather than to a hard error, which is the worst failure shape: silent and only noticed by users.characterLimit() was made a hard limit, then reverted to a soft limit in v5.0.1 (#36). The changelog also carries duplicated "Fixed" blocks and a stray Fixed.md, indicating release hygiene is inconsistent. A soft limit is the correct design, but anyone who adopted during the hard-limit window may hold the wrong mental model — and any documentation you write internally must state explicitly that characterLimit() does not enforce anything.Secondary risks: bus factor of one (single maintainer, 34 stars, 0 dependents — no ecosystem validation), and the removal of the character_seperator translation key in v5.2.0, which will quietly orphan any previously published translation override.
Key questions
characterLimit() need to enforce, or only inform? If enforcement matters, we pair it with maxLength(), a Laravel validation rule, and a correct DB column width — the package must not be the enforcement layer.max:, but ZWJ emoji and combining sequences still diverge from user-perceived length. Decide which definition we promise.aria-describedby, a spoken exceeded message and non-colour-only signalling ("(12 too many)"). Verify with our own screen-reader pass before claiming compliance.Stack fit
Clean fit for a modern Filament application on Laravel 12/13, PHP 8.2+, Filament 4 or 5 (Livewire 3 or 4). No database footprint, no queues, no scheduled work, no external services, no configuration file. It is a presentation-layer addition only, which is why the stack-fit question is really a version-alignment question rather than a capability question.
It is a poor fit for anything not on Filament forms (plain Blade, Livewire-only forms, Nova, Inertia), and irrelevant for Filament 3.2 unless pinned to 1.x, which should be treated as end-of-life for planning purposes.
Migration path
There is no data migration and no schema change. The migration is import hygiene:
^5.2 (do not float to *, and do not accept 4.x).character_seperator).If we want to avoid vendor imports spread across dozens of resources, wrap once: define App\Forms\Components\TextInput extends Schmeits\...\TextInput and import ours everywhere. That reduces a future swap to a single file, at the cost of a thin indirection layer. For a codebase likely to adopt this on more than a handful of fields, the wrapper is worth it; for a pilot, it is premature.
Compatibility
^5.2. Anything earlier in the 5.x line carries the RichEditor shadowing regression and the polling/hook leak.Sequencing
$set(), and respects maxLength.Maintenance
Ongoing maintenance is close to zero by design: no migrations, no config, no runtime services, plain CSS with Filament colour variables (no build step as of v5.2.0). The real maintenance cost is upgrade watching — we must review each release rather than blind-bumping, because this package's history includes a semantic API change (characterLimit() hard → soft) and a regression class (RichEditor feature shadowing) that would not surface in a green test suite. Budget a short review per Filament minor upgrade plus one per package release.
Governance risk is the bus factor: one maintainer, 34 stars, 0 dependents, no commercial backing. Mitigation is cheap — the package is small enough that forking and owning it internally is a realistic fallback (days, not weeks), and the MIT licence permits it. The import-level integration means even a full abandonment is a find-and-replace, not a rewrite. That asymmetry (low value, but also low exit cost) is what makes adoption defensible.
Support
Community-only, MIT, no SLA, no vendor contact. Issue #36 and #50 in the changelog show the maintainer is responsive and that fixes ship. The changelog, however, has duplicated blocks and a stray Fixed.md, so it is a signal of intent rather than a reliable audit trail — verify behaviour empirically rather than by reading release notes. Our internal documentation should state the verified version matrix and the soft-limit semantics; don't rely on the README, which still advertises a PHP floor that the release notes have moved past.
Scaling
Effectively free. Counting is client-side Alpine/DOM work with no queries, no server round-trips, no cache, no storage. It scales with the browser, not with the backend. The one historical scaling concern — a 500 ms polling loop and a leaked Livewire hook per editor instance — was removed in v5.2.0, so on ^5.2 a form with many counters should not accumulate listeners. Worth a quick sanity check on a form with several RichEditors, but this is not a capacity-planning item.
Failure modes
| Failure | Likelihood | Detection | Impact | Mitigation |
|---|---|---|---|---|
| RichEditor DOM selector changes upstream → counter freezes or reads 0 | Medium over time | Manual/visual only | Silent; users lose the affordance they were promised | Pin version, verify on Fibament upgrades, smoke test |
Team assumes characterLimit() enforces; oversize data reaches DB |
Medium at onboarding | Truncation error or silent cut | Data loss or 500s | Always pair with maxLength() + validation + column width; document soft-limit semantics |
| RichEditor capabilities silently disabled by a view override | Low on ^5.2, was real earlier |
Manual toolbar check | Feature regression invisible to tests | RichEditor verification gate; smoke test on toolbar |
| Counter and server disagree on length (ZWJ emoji, combining marks) | Low–medium | User complaint at the boundary | Confusing validation errors | Define the counting rule we promise; test boundary cases |
| Package abandoned / stale on next Filament major | Medium, longer horizon | Release cadence review | Forced fork or removal | Keep imports centralised; wrapper if adoption widens |
| Stale published translation overrides after key removal | Low | Missing/incorrect copy | Cosmetic | Don't publish translations unless overriding; review on upgrade |
Ramp-up
Near-zero for developers: one composer require, one import swap, one fluent method. Expect ~15 minutes to first working counter and ~1 hour to cover a resource set with conventions agreed. The ramp-up cost that matters is organisational, not technical: making sure every developer knows characterLimit() informs and maxLength() enforces, and that the RichEditor check is a release gate rather than an assumption. That is a short paragraph in the team's form-field conventions doc, and it is the highest-leverage artefact this adoption produces.
Recommendation. Adopt on ^5.2 for TextInput and Textarea, where the value (visible limits reduce user error) clearly exceeds the near-zero risk and the exit cost is a find-and-replace. Treat RichEditor as conditional: adopt only after the explicit capability verification passes on our Filament version, since that is where the package's coupling to framework internals is real and where its own history shows the only materially damaging regression. Keep the package out of the enforcement path entirely — it is a UX affordance, and the schema and validation rules remain the source of truth.
How can I help you explore Laravel packages today?