Skip to content
Blog

Text-to-Speech in Flutter Web: flutter_tts, Voices, and Browser Rules

Add spoken output to a Flutter web app with flutter_tts, covering voices, rate and pitch, progress callbacks, and browser user-gesture limits.

Published on • November 10, 2026

AI Assistant

Text-to-speech in Flutter web is a thin wrapper over the browser’s Web Speech API, and that wrapper mostly behaves like its native siblings with a few sharp edges. The flutter_tts package gives you one interface for speak, stop, pause, language, rate, volume, pitch, and voices across platforms, while the browser decides which voices exist and when you are allowed to start. This post covers both layers and how they interact.

Set up flutter_tts

Add the dependency and create one long-lived instance, because the plugin registers itself with the web engine and holds the speech synthesis state:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_tts: ^4.2.5
import 'package:flutter_tts/flutter_tts.dart';

final FlutterTts tts = FlutterTts();

Future<void> initSpeech() async {
  await tts.setLanguage('en-US');
  await tts.setSpeechRate(0.5);
  await tts.setVolume(1.0);
  await tts.setPitch(1.0);
  await tts.awaitSpeakCompletion(true);
}

The current release, 4.2.5, supports Android, iOS, macOS, web, and Windows, so the same file compiles for every target without conditional imports.

Speak, stop, and pause

speak() and stop() return a result int, where 1 means the call took effect, and pause() is supported on Android, iOS, Web, and Windows. On web, pause is a real browser speechSynthesis.pause() call, so it works without the onRangeStart() workaround that Android needs on SDK versions below 26.

Future<void> speakWelcome() async {
  final dynamic result = await tts.speak('Welcome back to the dashboard');
  if (result == 1) {
    // speaking
  }
}

Future<void> halt() => tts.stop();

One caveat for long content: Chrome has been reported to cut off synthesized speech after roughly 15 seconds of continuous text, so chunk long passages into sentences and chain them rather than handing a whole article to a single speak() call.

Choose languages and voices

Voices come from the operating system or browser, not from your app. getLanguages and getVoices return lists you can populate a picker from, and setVoice takes a map with at least name and locale keys:

Future<void> pickVoice() async {
  final dynamic languages = await tts.getLanguages;
  print('Languages: $languages');

  final dynamic voices = await tts.getVoices;
  for (final dynamic voice in voices as List) {
    final Map<String, dynamic> map = voice as Map<String, dynamic>;
    print('${map['name']} - ${map['locale']}');
  }

  await tts.setVoice({'name': 'Karen', 'locale': 'en-AU'});
}

Because voices are supplied by the platform, the exact set differs between Chrome, Edge, and Safari. If consistent, branded voices matter more than device-native quality, that is the point where a server-side TTS service with fixed voices becomes the better choice, especially for offline or long-form audio.

Track progress with handlers

flutter_tts exposes lifecycle handlers that keep your UI in sync: setStartHandler, setCompletionHandler, setProgressHandler, setErrorHandler, setCancelHandler, plus setPauseHandler and setContinueHandler on Android, iOS, and Web.

void bindHandlers(void Function(String word) onWord) {
  tts.setStartHandler(() {
    print('started');
  });
  tts.setCompletionHandler(() {
    print('finished');
  });
  tts.setProgressHandler((String text, int startOffset, int endOffset, String word) {
    onWord(word);
  });
  tts.setErrorHandler((dynamic msg) {
    print('tts error: $msg');
  });
  tts.setCancelHandler((dynamic msg) {
    print('tts cancelled');
  });
}

There is a real limitation here: progress updates are only supported for native speech synthesis, so you must use the default engine on web to receive them. The tracked upstream issue for that gap is Chromium bug 41195426. If progress callbacks arrive empty in your testing, that is the browser’s engine boundary rather than a bug in your handler wiring.

Know the browser’s rules

Under the hood, flutter_tts on web drives window.speechSynthesis and SpeechSynthesisUtterance, and the browser imposes two rules that surprise people. First, Chrome requires a user activation before speak() is allowed, and returns a not-allowed error when the page has no recent gesture; since Chrome 71 this has been enforced. Second, iOS Safari expects the very first speak() call to happen synchronously inside a tap or click handler, which is why many apps fire a silent dummy utterance on the first button press to unlock the engine.

// package:web, for when you need the raw API
import 'dart:js_interop';
import 'package:web/web.dart' as web;

void speakDirectly(String text) {
  final utterance = web.SpeechSynthesisUtterance(text);
  utterance.lang = 'en-US';
  utterance.rate = 1.0;
  utterance.onend = ((web.Event _) {
    print('done');
  }).toJS;
  web.window.speechSynthesis.speak(utterance);
}

The raw API mirrors flutter_tts closely: speechSynthesis offers speak, cancel, pause, resume, and getVoices, with read-only speaking, paused, and pending flags, while each utterance carries text, lang, voice, rate, pitch, and volume and fires start, end, boundary, and error events. Keep the raw path as an escape hatch for debugging, and use flutter_tts for the cross-platform code you ship.

Test across browsers

Verification here means real devices, not just one desktop browser. Check Chrome, Edge, and Safari: confirm the voice list populates, speak after a button tap, pause and resume mid-sentence, and verify the not-allowed error appears when you call speak() without a gesture on purpose. Then test a long paragraph to confirm your chunking avoids the 15-second cut-off, and test a non-English locale to make sure the voice you selected actually covers it.

Takeaway: Ship text-to-speech in Flutter web with flutter_tts, drive it from a user gesture, chunk long text to dodge Chrome’s cut-off, and fall back to server-side TTS when you need guaranteed voices or progress.

Sources