Live search with Tom Select, zero custom JavaScript
Symfony UX Autocomplete connects Doctrine entities, custom search logic and external APIs through a single form field type, delivering server side rendering, Stimulus wiring and Tom Select on the frontend without writing a single custom JavaScript module.
Table of Contents
- 1. Why Symfony UX Autocomplete exists
- 2. Installation and basic configuration
- 3. Enabling autocomplete directly in EntityType
- 4. A custom AutocompleteField class for complex searches
- 5. Wiring in external APIs instead of Doctrine entities
- 6. Configuring Tom Select: multiple, preload, rendering
- 7. Styling with Tailwind, no extra CSS framework
- 8. Performance: debounce, minimum length and caching
- 9. Symfony UX Autocomplete compared
- 10. Summary
- 11. FAQ
1. Why Symfony UX Autocomplete exists
Symfony UX Autocomplete solves a problem that shows up in almost every larger form: a dropdown with thousands of entries no longer makes sense as a plain select field. Before Symfony UX, the usual fix was to wire up select2 or Choices.js by hand, write custom JavaScript initialization and implement the server communication with a hand rolled fetch call. Every project ended up with its own, slightly different solution and its own set of bugs.
Symfony UX Autocomplete wraps exactly this task into a reusable bundle. The form field type automatically generates the matching Stimulus attribute, the bundle ships a ready made controller that initializes Tom Select, and the server communication runs through a generated controller endpoint that wraps Doctrine repository queries. The result: live search inside forms without a single line of custom JavaScript, while still offering clearly defined PHP extension points for anything beyond the default case.
The difference from a plain JavaScript library lies in the integration depth. Symfony UX Autocomplete understands Doctrine entities, validation rules and CSRF protection out of the box, because it is part of the Symfony form system rather than an independent frontend widget bolted on the side.
2. Installation and basic configuration
Installing Symfony UX Autocomplete happens through Composer, and on AssetMapper projects the JavaScript dependencies are wired up automatically through the Symfony Asset Mapper. On Encore projects you additionally need to run yarn or npm to include Tom Select in the build. After installation the bundle automatically registers a Stimulus controller and exposes the form field type Symfony\UX\Autocomplete\Form\AutocompleteField along with the autocomplete option for EntityType.
One important point during basic configuration: Symfony UX Autocomplete automatically registers an internal controller under /_ux_entity_autocomplete/{alias} for every entity that uses autocomplete. This endpoint does not need to be added manually to routes.yaml, but the security layer still treats it like any other route. Anyone using autocomplete on sensitive entities therefore has to explicitly check that the generated endpoint is covered by firewalls and access control rules.
# Install Symfony UX Autocomplete
composer require symfony/ux-autocomplete
# AssetMapper projects: assets are wired automatically
bin/console importmap:require symfony/ux-autocomplete
# Encore projects only: build the JS integration
yarn add @symfony/ux-autocomplete --dev
yarn encore dev
3. Enabling autocomplete directly in EntityType
The fastest way into Symfony UX Autocomplete is the autocomplete option directly on EntityType. A plain dropdown with thousands of products or customers becomes a live search field without writing a dedicated class. Symfony generates the matching endpoint behind the scenes, searches through the entity's repository and delivers the results to Tom Select as JSON.
This simple form of Symfony UX Autocomplete fits the default case: searching across one or two fields, no complex sorting logic, no per row permission checks. Once the search logic gets more involved, for example combining several fields or checking permissions per row, you switch to the custom AutocompleteField class covered in section four.
// src/Form/OrderType.php
namespace App\Form;
use App\Entity\Customer;
use App\Entity\Order;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
final class OrderType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('customer', EntityType::class, [
'class' => Customer::class,
'choice_label' => 'fullName',
// Enable Symfony UX Autocomplete for this field
'autocomplete' => true,
'placeholder' => 'Search for a customer...',
]);
}
public function getBlockPrefix(): string
{
return '';
}
}
4. A custom AutocompleteField class for complex searches
Once the search behind Symfony UX Autocomplete needs to do more than a plain LIKE query on one field, you create a custom class that extends AbstractType and implements the EntityAutocompleteFieldInterface. This class defines a createFilteredQueryBuilder method where arbitrarily complex Doctrine query builder logic can live, including joins across related entities, relevance based sorting, or filtering by the currently logged in user.
The decisive advantage of this variant of Symfony UX Autocomplete is that the search logic lives in exactly one place in the codebase and can be reused across multiple forms without duplicating options. The class can additionally override a getLabel method to assemble the displayed label per result individually, for example customer name plus customer number plus city on a single line.
// src/Form/CustomerAutocompleteField.php
namespace App\Form;
use App\Entity\Customer;
use App\Security\CurrentUserProvider;
use Doctrine\ORM\QueryBuilder;
use Symfony\UX\Autocomplete\Doctrine\EntityAutocompleteFieldInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
final class CustomerAutocompleteField extends EntityType implements EntityAutocompleteFieldInterface
{
public function __construct(
private readonly CurrentUserProvider $currentUserProvider,
) {
}
public function getLabel(object $entity): string
{
/** @var Customer $entity */
return sprintf('%s (%s, %s)', $entity->getFullName(), $entity->getCustomerNumber(), $entity->getCity());
}
public function createFilteredQueryBuilder(QueryBuilder $queryBuilder, string $query): void
{
$tenantId = $this->currentUserProvider->getCurrentTenantId();
$queryBuilder
->andWhere('entity.tenant = :tenantId')
->setParameter('tenantId', $tenantId)
->andWhere('entity.fullName LIKE :query OR entity.customerNumber LIKE :query')
->setParameter('query', '%' . $query . '%')
->orderBy('entity.fullName', 'ASC')
->setMaxResults(20);
}
public function getOptionsForAutocomplete(): array
{
return [
'class' => Customer::class,
'choice_label' => 'fullName',
];
}
}
5. Wiring in external APIs instead of Doctrine entities
Not every search behind Symfony UX Autocomplete has to run against your own database. For product data from an external PIM system, address suggestions from a geodata API, or item searches against Elasticsearch, the bundle lets you register a custom controller as the data source instead of relying on a Doctrine repository. The controller only needs to return the JSON shape that Tom Select expects for its result list.
This approach to Symfony UX Autocomplete fully decouples the frontend component from the data source. Instead of pointing at the internal entity endpoint, the Stimulus attribute points directly at a custom route that internally uses an HTTP client for the external API, normalizes the result and returns it as JSON. From the user's perspective the interaction looks identical to the entity based variant, while a completely different request runs under the hood.
// src/Controller/ProductSearchAutocompleteController.php
namespace App\Controller;
use App\Client\ExternalPimClient;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
final class ProductSearchAutocompleteController extends AbstractController
{
public function __construct(
private readonly ExternalPimClient $pimClient,
) {
}
#[Route('/autocomplete/products', name: 'app_autocomplete_products', methods: ['GET'])]
public function __invoke(Request $request): JsonResponse
{
$query = (string) $request->query->get('query', '');
if (mb_strlen($query) < 2) {
return new JsonResponse(['results' => []]);
}
$products = $this->pimClient->search($query, limit: 15);
// Tom Select expects a "results" array with id/text pairs
$results = array_map(
static fn (array $product): array => [
'id' => $product['sku'],
'text' => sprintf('%s (%s)', $product['name'], $product['sku']),
],
$products,
);
return new JsonResponse(['results' => $results]);
}
}
6. Configuring Tom Select: multiple, preload, rendering
Under the hood, Symfony UX Autocomplete uses Tom Select as its rendering library. Most Tom Select options can be controlled through data attributes on the generated element or through options on the form field type, without writing a custom Stimulus controller class. The multiple option enables tag based multi selection, and the tom_select_options option passes an associative array straight through to the Tom Select instance, for example for custom render callbacks.
For cases where a preselected value needs to be visible right on page load, Symfony UX Autocomplete supports preloading the current value by rendering its label on the server. This avoids the usual brief moment where only the ID is visible until JavaScript fetches the matching text. Anyone who needs custom render templates for the dropdown options, for example an avatar image with extra details, can supply render_option and render_item as JavaScript functions through the options.
{# templates/order/new.html.twig #}
{{ form_start(form) }}
{{ form_row(form.customer, {
attr: {
'data-tom-select-multiple': false,
'data-tom-select-preload': 'focus'
}
}) }}
{{ form_row(form.products, {
attr: { 'data-tom-select-multiple': true }
}) }}
{{ form_end(form) }}
7. Styling with Tailwind, no extra CSS framework
Tom Select ships with its own default theme that visually clashes with a Tailwind driven, Hyvä style frontend. Instead of importing the full Tom Select CSS, Symfony UX Autocomplete projects deliberately override the generated Tom Select CSS classes with custom Tailwind utility classes. Tom Select produces predictable class names such as ts-control, ts-dropdown and ts-wrapper that can be adapted to your own design system through a thin CSS layer.
The important part of this approach to Symfony UX Autocomplete is to load only the minimal, unstyled base file from Tom Select rather than the full default stylesheet, and to layer your own Tailwind classes on top of it. This meaningfully shrinks the CSS bundle size and prevents conflicts between Tailwind's utility first approach and Tom Select's prebuilt classes.
/* assets/styles/autocomplete.css */
/* Minimal overrides on top of Tom Select's base (unstyled) CSS */
.ts-wrapper.single .ts-control {
@apply rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm shadow-sm;
}
.ts-wrapper.focus .ts-control {
@apply border-gray-700 ring-2 ring-gray-200;
}
.ts-dropdown {
@apply mt-1 rounded-lg border border-gray-200 bg-white shadow-lg;
}
.ts-dropdown .option.active {
@apply bg-gray-100 text-gray-900;
}
8. Performance: debounce, minimum length and caching
Without tuning, Symfony UX Autocomplete sends a request to the server on every single keystroke, which creates an unnecessary number of parallel requests when typing quickly. The bundle debounces input by default, and the wait time can be adjusted through the debounce option in milliseconds. On top of that, a configurable minimum search input length prevents thousands of database rows from being loaded after just one or two characters.
On the server side it is worth adding a short cache for frequently repeated search queries, for example through the Symfony cache layer with a TTL of a few seconds. This reduces database load for popular search terms without results feeling noticeably stale. Anyone running Symfony UX Autocomplete against an external API should use the cache to avoid exceeding that API's rate limits as well.
9. Symfony UX Autocomplete compared
Before committing to Symfony UX Autocomplete, it is worth looking at the alternatives commonly found in existing projects. Each approach has different consequences for maintenance effort, bundle size and integration depth with the Symfony form system.
| Approach | Integration | Custom JS Code | Bundle Size |
|---|---|---|---|
| select2 wired manually | Loose, custom fetch needed | A lot | Medium |
| Choices.js manually | Loose, custom fetch needed | A lot | Medium |
| Symfony UX Autocomplete | Deep, form system native | None | Low |
| Custom Stimulus controller | Medium, hand built | A lot | Variable |
| Native HTML datalist | Very loose | None | None |
The native datalist solution without any library at all seems tempting because it costs zero extra bytes of JavaScript, but in practice it fails on server side search, missing debouncing and inconsistent browser rendering. Symfony UX Autocomplete sits right between the two extremes: minimal custom code, yet full control over server logic and rendering exactly where it is needed.
Mironsoft
Symfony development with a modern UX frontend
Forms that feel like an SPA, without writing any custom JavaScript?
We integrate Symfony UX Autocomplete into existing forms, build custom AutocompleteField classes for complex searches, and connect external APIs cleanly to your Symfony project.
UX audit
Reviewing existing forms for autocomplete potential
Implementation
Custom AutocompleteField classes and external API wiring
Tailwind styling
Integrating Tom Select seamlessly into your design system
10. Summary
Symfony UX Autocomplete solves the classic problem of oversized dropdown lists by uniting Doctrine repositories, custom search logic and external APIs under one consistent Stimulus integration built on Tom Select. The simple case only needs the autocomplete option on EntityType, while more demanding requirements are handled by a custom AutocompleteField class with full query builder control over the search. External data sources can be wired in through a custom controller without touching the frontend integration at all.
The biggest advantage over manually wired JavaScript libraries lies in maintainability: Symfony UX Autocomplete stays part of the Symfony ecosystem, benefits from security updates through Composer, and needs no separate build process for its core functionality. Configuring debounce, minimum length and a server side cache on top gives you a live search that behaves just as reliably under load as it does under normal conditions.
Symfony UX Autocomplete — The essentials at a glance
Quick start
The autocomplete: true option on EntityType covers the default case, with zero custom JavaScript code.
Complex searches
A custom AutocompleteField class with createFilteredQueryBuilder for joins, permissions and custom labels.
External sources
A custom controller acting as data source for PIM systems, Elasticsearch or geodata APIs.
Performance
Debounce, minimum length and a server cache prevent unnecessary database or API load.