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

Ux Typed Laravel Package

symfony/ux-typed

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install the bundle

    composer require symfony/ux-typed
    

    Like all Symfony UX packages, this pulls in symfony/stimulus-bundle (if not already present). Make sure your assets/controllers.json is present — the package auto-registers its typed controller through Flex/CONFIG.

  2. Get the JS dependency

    • With AssetMapper: php bin/console importmap:require typed.js
    • With Webpack Encore: npm install typed.js The Stimulus controller imports typed.js at the top, so it will fail to load silently if the package isn't installed.
  3. Confirm assets build

    php bin/console asset-map:compile   # AssetMapper
    # or
    npm run dev                          # Encore
    
  4. First use case — an animated hero headline that cycles through selling points:

    <h1 {{ stimulus_controller('typed', {
        strings: ['We build Symfony apps.', 'We ship faster.', 'We scale.'],
    }) }}></h1>
    

    The controller instantiates a Typed object on the element and lets Typed.js own its text content. Load the page — you should see the cursor blink and the strings typed out.

  5. Where to look first

    • vendor/symfony/ux-typed/assets/src/controller.js (or .ts) — the actual data-value names the controller exposes. This is the source of truth for what you can pass in stimulus_controller().
    • Typed.js docs (https://mattboldt.github.io/typed.js/) — every option in options is passed straight through.

Implementation Patterns

Inline strings (the 80% case)

strings is the one value the controller knows about natively; anything else goes in options:

<span {{ stimulus_controller('typed', {
    strings: ['Hello', 'Bonjour', 'Hola'],
    options: { typeSpeed: 60, backSpeed: 30, loop: true },
}) }}></span>

Equivalent plain-HTML form (useful when strings come from data):

<span data-controller="typed"
      data-typed-strings-value='["Hello","Bonjour","Hola"]'
      data-typed-options-value='{"typeSpeed":60,"loop":true}'></span>

Data-driven strings from a controller

Build the array in PHP and JSON-encode it — no escaping gymnastics needed since Stimulus reads the attribute as JSON:

{% set taglines = product.taglines|map(t => t.text) %}
<h2 {{ stimulus_controller('typed', {
    strings: taglines,
    options: { smartBackspace: true, backDelay: 1500 },
}) }}></h2>

If you're rendering from a JSON endpoint instead, hydrate on connect:

// a wrapper controller
import TypedController from './typed_controller';

…or simply render the markup server-side. For a "one controller to rule them all" pattern, create a small wrapper Stimulus controller that fetches strings and then dispatches a typed:start event to the underlying typed controller.

Composing with other UX packages

Because stimulus_controller() returns a full data-controller attribute, keep it on its own element if another controller wants the same node. Common combination: stimulus_controller('typed', ...) plus a scroll-reveal or intersection-observer observer controller on the parent, so Typed doesn't fire until visible.

Reusable Twig macro

Wrap the noise once:

{% macro typed(strings, options = {}) %}
    <span {{ stimulus_controller('typed', { strings: strings, options: options }) }}></span>
{% endmacro %}

Then: {{ _self.typed(['Hi', 'Hello'], { loop: true }) }}.

Multi-instance pages

Stimulus binds per-element, so you can have a headline, a subheading, and a footer tagline all running independent Typed instances on the same page. Just don't share the same strings array by reference in a way that mutates it — Stimulus reads it once at connect.

Controlling the animation

Typed.js exposes start(), stop(), reset(), destroy(). You usually don't need them from Twig, but if you do, add an event bridge:

// custom_typed_controller.js — extends or reuses the package controller class

or use a tiny controller that grabs the element and calls into the instance. The package controller keeps the instance on this.typed in its own instance, so the cleanest approach is to write a wrapper controller and import Typed from 'typed.js' yourself if you need imperative control.

Turbo / SPA considerations

On Turbo navigation Stimulus connect()/disconnect() fire per page visit, and the package controller's disconnect should call this.typed.destroy() to stop the timer. If you're customizing, ensure you destroy the instance, otherwise the RAF loop leaks across navigations.

Gotchas and Tips

  • The element's content is replaced. Typed.js owns the text node of the target element. If you put fallback text inside the tag, it will be wiped at runtime — useful for progressive enhancement (screen readers / no-JS get the fallback), but the same fallback will flash on load. Mitigate with opacity:0 on the element plus a .typed-loaded class toggled via the controller's connect/typed:start event.
  • options shape must be valid JSON. stimulus_controller() will json_encode for you, but if you hand-write data-typed-options-value, single quotes or trailing commas break it silently — the controller throws inside connect(), and Stimulus swallows nothing, so check the console.
  • Verify the value name in the source. The controller's static values block is authoritative. If you don't see options in it, use the individual option data attributes (data-typed-type-speed-value etc.) — the naming will match the JS option in kebab-case. Guessing at value names is the #1 source of "nothing happens".
  • typed.js import fails → whole page's Stimulus boot stalls if the controller is eager-loaded. With AssetMapper, forgetting importmap:require typed.js is the common first bug. Symptom: no typing, console error Failed to resolve module specifier "typed.js".
  • Accessibility / prefers-reduced-motion. Typed.js has no built-in reduced-motion guard. Add CSS:
    @media (prefers-reduced-motion: reduce) {
        [data-controller="typed"] .typed-cursor { display: none; }
    }
    
    and consider rendering the first string statically and skipping the controller entirely when motion is reduced (detect server-side via Sec-CH-Prefers-Reduced-Motion or add a small Stimulus wrapper that calls typed.stop()).
  • Layout shift. Typed.js resets the element's height on each string. Reserve space with min-height on the wrapper, or use a fixed-height container, otherwise the page jiggles on every backspace.
  • Cursor CSS. The cursor is .typed-cursor — blink animation is inline JS but styling is on you. If you styled the element font-family weirdly, the cursor inherits it.
  • HTML in strings. strings can contain markup only if contentType: 'html' and you accept innerHTML-style risks. Never feed user-generated content into strings — it's effectively innerHTML for the typed portion.
  • Extension points. There is no PHP-side config (no config/packages/ux_typed.yaml). Customization is entirely via: (a) options data value, (b) overriding the controller in assets/controllers/typed_controller.js and pointing importmap/controllers.json at your version, or (c) subscribing to the DOM and driving Typed yourself. For a project with three or more typed elements and shared defaults, a project-local wrapper controller is cleaner than repeating options in every Twig call.
  • Testing. In functional tests, stimulus_controller() output is just data attributes — assert on those, not on rendered animation. E2E (Panther/Playwright) needs typed.js actually loaded; a WebDriverWait on the cursor element is the reliable signal that the controller connected.
  • Version skew. The package pins/expects a typed.js major. If you importmap:require typed.js and get v3 while the controller was written for v2, option names like smartBackspace still exist but stringsElement and onStringTyped signatures differ. Check assets/package.json in the vendor dir if the animation misbehaves.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
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