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

Bootstrap 3 Typeahead Laravel Package

bassjobsen/bootstrap-3-typeahead

A Bootstrap 3 autocomplete plugin that adds typeahead suggestions to text inputs. Forked from Twitter's typeahead, it offers flexible data sources, custom display templates, and easy integration with Bootstrap forms.

Deep Wiki
Context7

Getting Started

bassjobsen/bootstrap-3-typeahead is not a Laravel package — it's a jQuery plugin that restores Bootstrap 2's $.fn.typeahead after Bootstrap 3 removed it. There is no service provider, facade, or config file. Your integration surface is: one JS asset, one Blade input, one JSON route.

Install

npm install bootstrap-3-typeahead   # v4.0.2, published 2016-09-26

or via CDN (drop the defer if your init script runs inline):

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/bootstrap3-typeahead.min.js"></script>

Wire it into Vite. The plugin's IIFE reads window.jQuery at evaluation time, so order matters more than imports look like they should:

// resources/js/app.js
import $ from 'jquery'
window.$ = window.jQuery = $          // must exist BEFORE the next import
import 'bootstrap-3-typeahead'        // attaches $.fn.typeahead

If the optimizer hoists it, add optimizeDeps: { include: ['bootstrap-3-typeahead'] } in vite.config.js, and import the file by its full name (bootstrap-3-typeahead/bootstrap3-typeahead.min.js) if the package main field misbehaves in your bundler.

Where to look first: the package README's options table (source, items, minLength, updater, matcher, sorter, highlighter, displayText, item, autoSelect, afterSelect). That table is the documentation; there is no other documentation and no changelog after 2016.

First use case — a Blade form with a server-side lookup

// routes/web.php
Route::get('/users/search', [UserSearchController::class, 'index'])->name('users.search');

// Controller
public function index(Request $request)
{
    return User::query()
        ->where('name', 'like', $request->string('q').'%')
        ->orderBy('name')
        ->limit(10)
        ->get(['id', 'name']);   // plain array of objects — no wrapping key
}
<input type="text" id="user-search" class="form-control" autocomplete="off" placeholder="Search users…">
<input type="hidden" name="user_id" id="user-id">

<script>
$(function () {
  $('#user-search').typeahead({
    minLength: 2,
    items: 10,
    displayText: function (item) { return item.name },
    source: function (query, process) {
      $.get('{{ route('users.search') }}', { q: query }, function (data) {
        process(data)              // must be called, even with []
      })
    },
    afterSelect: function (item) {
      $('#user-id').val(item.id)
    }
  })

  $('#user-search').on('input', function () { $('#user-id').val('') })
})
</script>

That's the whole loop: type → GET → process(arrayOfObjects) → displayText renders → afterSelect stores the ID.

Implementation Patterns

Debounce inside source, not outside it. The plugin fires source on every keystroke above minLength; there is no built-in debounce option. Wrap it yourself and abort the in-flight request — out-of-order AJAX responses otherwise overwrite newer results:

let xhr = null, timer = null
source: function (query, process) {
  clearTimeout(timer)
  timer = setTimeout(function () {
    if (xhr) xhr.abort()
    xhr = $.get(url, { q: query }, function (data) { process(data) })
  }, 150)
}

Hidden-ID field is the canonical pattern. updater controls the text written into the input; afterSelect fires after selection and is the right hook for side effects (setting a hidden user_id, filling sibling fields, enabling a submit button). Returning a value from updater replaces the input text — return item.name there and never the ID, or users see raw IDs.

Custom item markup with item. Useful for avatars, email addresses, badges:

item: function (item) {
  return '<a class="dropdown-item d-flex align-items-center">'
       + '<img src="' + item.avatar + '" width="20" class="me-2">'
       + '<span>' + $('<div>').text(item.name).html() + '</span></a>'
}

Escape anything user-controlled — the plugin injects the return value with .html().

Local arrays for small, static sets (countries, statuses, tags) — skip the HTTP round-trip entirely:

$('#tag').typeahead({ source: ['urgent', 'billing', 'bug', 'feature'] })

Reusable initializer driven by data attributes, so Blade partials don't each ship a <script>:

<input data-typeahead data-url="{{ route('users.search') }}" data-target="#user-id">
$('[data-typeahead]').each(function () {
  const $el = $(this)
  $el.typeahead({ minLength: 2, displayText: i => i.name, source: (q, process) =>
    $.getJSON($el.data('url'), { q }, process),
    afterSelect: i => $($el.data('target')).val(i.id)
  })
})

Livewire / Turbo. DOM morphing destroys the plugin's state. Mark the wrapper wire:ignore, and re-init on livewire:navigated / turbo:load, not just DOMContentLoaded. Dispatching $dispatch('user-selected', { id }) from afterSelect is cleaner than mutating hidden inputs across framework boundaries.

"Show options on focus" isn't a feature — minLength: 1 means a click alone does nothing. Set minLength: 0 and make the endpoint tolerate q='', or trigger the fetch manually on focus.

autoSelect: false if Enter should submit the form rather than grab the top suggestion. Default behavior highlights the first item, which surprises users in search-then-submit forms.

Multi-field dependency chains (country → city): in the first field's afterSelect, clear and re-typeahead('destroy')-then-reinit the second, or store the chosen parent in a closure the second field's source reads. Avoid passing state through DOM text.

Gotchas and Tips

Unmaintained since 2016 — treat that as the headline risk. No ESM build, no TypeScript types, no accessibility work, no Bootstrap 5 awareness. For greenfield work, Tom Select or Algolia Autocomplete are better investments; choose this one when you want zero dependencies beyond jQuery and a tiny surface area. It still works fine on jQuery 3.7.

$.fn.typeahead collides with Twitter's typeahead.js and with Bootstrap 2's bundled plugin. If another package in your bundle also defines it, whichever loads last wins and the loser's options silently no-op. Grep typeahead across package.json before debugging anything weird.

Bootstrap 4/5 styling breaks the menu. The plugin emits <ul class="typeahead dropdown-menu"><li><a>…</a></li></ul> — BS4+ expects .dropdown-item on the anchor, so you get unstyled, padded-wrong rows. Minimal shim:

.typeahead.dropdown-menu { width: 100%; max-height: 320px; overflow-y: auto; }
.typeahead.dropdown-menu > li > a { display: block; padding: .375rem .75rem; clear: both; white-space: nowrap; }
.typeahead.dropdown-menu > li.active > a { background: var(--bs-primary); color: #fff; }

Clipping inside modals and scroll containers. The menu is absolutely positioned within the input's positioning context and will be cut off by overflow: hidden on any ancestor (.modal-body, .table-responsive, cards). Fixes: give the input's immediate wrapper position: relative, remove the offending overflow, or reparent the menu — but reparenting loses the plugin's own offset math, so prefer the CSS route.

Always call process(), even on empty results. If the callback never fires (error path, aborted request, a return response()->json(null)), later queries can leave the widget stuck with no menu. Do process([]) in every terminal branch, including $.get().fail(...).

Return a bare JSON array. Laravel's paginated responses (data: [...]) and resource()->collection() wrappers will not work — process() expects the array itself. ->get(['id','name']) or ->map(fn ($u) => [...])->values() before returning.

matcher, sorter, highlighter are irrelevant for server-side sources. The server already filtered and ordered; leave them alone. For local arrays, highlighter wraps matches in <strong>, which is the default and usually fine — but it operates on the display string, so objects need displayText before highlighting behaves.

Vite + jQuery global order is the #1 setup failure. Symptom: $(...).typeahead is not a function in production only. The window.jQuery = $ assignment must execute before the plugin module is evaluated; if you split vendor chunks, add a tiny jquery-global.js entry that every other entry imports first.

CSRF: source uses GET, so you're unaffected. If you POST, add $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content } }) — the plugin has no header options of its own.

No teardown API worth relying on. For SPA/livewire re-inits, the reliable path is: remove the DOM node, insert a fresh one, re-init. Also .off('input') your own hand-attached handlers, which the plugin won't clean up.

Accessibility is on you. The rendered input has no role="combobox", aria-expanded, or aria-activedescendant, and no screen-reader announcements. If WCAG matters, add role="combobox" aria-autocomplete="list" aria-controls="…" to the input manually and maintain a visually hidden live region from afterSelect — or pick a different library.

Debugging tip: because everything is keyup-driven, log inside source (console.log(query, data.length)) before inspecting CSS. Nine out of ten "it doesn't show" reports are an empty array, a missing displayText on objects, or a swallowed error in the GET.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings