Flutter superscript and subscript text how to use.

Anna Muzykina
10 min readSep 3, 2023

--

Subscript and superscript are typically used to format text with smaller characters below or above the body text baseline, respectively.

Recently, I was faced with the need to display formulas on the screen. You might say that out-of-the-box Flutter features like FontFeature.superscripts() and .subscripts() might be enough, but sometimes those features alone are not enough.

In this article, I will demonstrate different ways to create subscript and superscript text.

Source code on GitHub subscript_superscript.

1. Firstly, in Flutter, you can create subscript and superscript text using FontFeature.subscripts()

The fontFeatures property with FontFeature.subscripts() is a feature in Flutter that allows you to enable OpenType font features, such as enabling subscript or superscript glyphs in supported fonts. This property is available in the TextStyle widget and can be used to apply subscript or other font features to the text.

Here’s an example of how to use fontFeatures to enable subscripts:

You can copy this code to try from here:

import 'package:flutter/material.dart';
import 'dart:ui';

void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'Flutter subscripts/superscripts'),
);
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});

final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [


RichText(
text: const TextSpan(
children: [
TextSpan(
text: 'H',
style: TextStyle(
color: Colors.black,
fontSize: 30,
)),
TextSpan(
text: '2',
style: TextStyle(
color: Colors.black,
fontSize: 30,
fontFeatures: [FontFeature.superscripts()])),
TextSpan(
text: 'O',
style: TextStyle(color: Colors.black, fontSize: 30)),
],
),
),


]
),
),
);
}
}

When you run this code, it will display the text “H₂O” with the ‘2’ rendered as a superscript above the baseline due to the FontFeature.superscripts() applied to the second TextSpan. The rest of the text ('H' and 'O') will be displayed with a font size of 30 and a regular baseline alignment.

I used the RichText widget to create formatted text with different styles within a single text element. Let's break down this code step by step:

  1. RichText Widget: You are using the RichText widget to display text with different styles.
  2. TextSpan Children: Within the RichText widget, you define a list of TextSpan children inside the TextSpan widget's children property. Each TextSpan represents a portion of the text with specific formatting.
  3. First TextSpan: The first TextSpan represents the letter 'H'. It has TextStyle a black color and a font size of 30. There are no font features applied to this part of the text.
  4. To use FontFeature.superscripts()You need to import library ‘dart:ui’
import 'dart:ui';

5. Second TextSpan: The second TextSpan represents the digit '2'. It also has a black color and a font size of 30, but in addition, you have applied FontFeature.superscripts() to this TextStyle. This will render the '2' as a superscript, making it appear above the baseline.

6. Third TextSpan: The third TextSpan represents the letter 'O'. It has a black color and a font size of 30, but there are no font features applied to it.

2. Using Transform.translate, Unicode character

What if we need to display the text such as “a logₐb = b” with the ‘logₐb’ portion formatted as a superscript due to the translation offset:

Here's an example of used code to realize such an option:

RichText(
text: TextSpan(
children: [
const TextSpan(
text: 'a',
style: TextStyle(
color: Colors.black,
fontSize: 30,
),
),
WidgetSpan(
child: Transform.translate(
offset: const Offset(0.0, -15.0),
child: const Text(
'log\u2090b',
style: TextStyle(fontSize: 14, color: Colors.red),
),
),
),
const TextSpan(
text: '= b',
style: TextStyle(color: Colors.black, fontSize: 30),
),
],
),
)

Here’s what this code does:

  1. RichText Widget: You are using the RichText widget to display text with different styles and include a widget span.
  2. TextSpan Children: Within the RichText widget, you define a list of TextSpan children inside the TextSpan widget's children property. Each TextSpan represents a portion of the text with specific formatting.
  3. First TextSpan: The first TextSpan represents the letter 'a'. It has a black color and a font size of 30.
  4. WidgetSpan: The second child on the TextSpan children's list is a WidgetSpan. A WidgetSpan allows you to include a widget as part of the text.
  5. Transform.translate: Inside WidgetSpan, you have a Transform.translate widget. This widget is used to translate its child widget, which is a Text widget, by an offset. In this case, you are translating it vertically by -15.0 logical pixels. This offset makes the content of the Text widget appears as a superscript, positioned above the baseline.
  6. Superscript Text: Inside Transform.translate, you have a Text widget with the content 'logₐb'. The 'ₐ' character (Unicode U+2090) is a subscript 'a', which appears as a superscript in this context due to the translation offset applied.
  7. Text Style: The Text widget inside the WidgetSpan has a font size of 14 and a black color.
  8. Third TextSpan: The third TextSpan represents the text '= b'. It has a black color and a font size of 30.

When you run this code, it will display the text “a logₐb = b” with the ‘logₐb’ portion formatted as a superscript due to the translation offset applied by the Transform.translate widget. The rest of the text is displayed with regular formatting.

In the code above I used the ‘ₐ’ character (Unicode U+2090) from unicode_map which is a Python dictionary that contains character mappings.

In this example, I just used Unicode \u2090 from the Python dictionary to realize the subscript ‘a’ (ₐ) between log and b :

At the end of this article, you can find more about this dictionary, a list of characters with subscript/superscript properties, and how to use it in Flutter.

3. Also you can copy and paste from the Wikipedia page Unicode subscripts and superscripts, here you can find different specific symbols ‘ʋ ᶹ’, ‘ɱ ᶬ’, ‘ɣ ˠ’ :

 const Text('Cₘₐₓ - Cₘᵢₙ', style: TextStyle(fontSize: 30)),

Consolidated for cut-and-pasting purposes, the Unicode standard defines complete sub- and super-scripts for numbers and common mathematical symbols ( ⁰ ¹ ² ³ ⁴ ⁵ ⁶ ⁷ ⁸ ⁹ ⁺ ⁻ ⁼ ⁽ ⁾ ₀ ₁ ₂ ₃ ₄ ₅ ₆ ₇ ₈ ₉ ₊ ₋ ₌ ₍ ₎ ), a full superscript Latin lowercase alphabet except q ( ᵃ ᵇ ᶜ ᵈ ᵉ ᶠ ᵍ ʰ ⁱ ʲ ᵏ ˡ ᵐ ⁿ ᵒ ᵖ ʳ ˢ ᵗ ᵘ ᵛ ʷ ˣ ʸ ᶻ ), a limited uppercase Latin alphabet ( ᴬ ᴮ ᴰ ᴱ ᴳ ᴴ ᴵ ᴶ ᴷ ᴸ ᴹ ᴺ ᴼ ᴾ ᴿ ᵀ ᵁ ⱽ ᵂ ), a few subscripted lowercase letters ( ₐ ₑ ₕ ᵢ ⱼ ₖ ₗ ₘ ₙ ₒ ₚ ᵣ ₛ ₜ ᵤ ᵥ ₓ ), and some Greek letters ( ᵅ ᵝ ᵞ ᵟ ᵋ ᶿ ᶥ ᶲ ᵠ ᵡ ᵦ ᵧ ᵨ ᵩ ᵪ ). Note that since these glyphs come from different ranges, they may not be of the same size and position, depending on the typeface. As you can see ‘Cₘᵢₙ’ on screen looks not very nice

So better to use Unicodes here as well:

const Text('C\u2098\u2090\u2093 - C\u2098\u1D62\u2099'

But the better solution is to use Transform.translate , offset and fontSize:

 RichText(
text: TextSpan(
style: const TextStyle(color: Colors.black, fontSize: 30),
children: [
const TextSpan(
text: 'C',
),
WidgetSpan(
child: Transform.translate(
offset: const Offset(0.0, 3.0),
child: const Text(
'min',
style: TextStyle(fontSize: 15, color: Colors.black),
),
),
),
const TextSpan(
text: ' - ',
),
const TextSpan(
text: 'C',
style: TextStyle(fontSize: 30),
),
WidgetSpan(
child: Transform.translate(
offset: const Offset(0.0, 3.0),
child: const Text(
'max',
style: TextStyle(fontSize: 15, color: Colors.black),
),
),
),
],
),
),

Let’s dive deep into this dictionary and how we can use it in Flutter:

Each entry in the dictionary consists of a key-value pair.

  unicode_map = {
#Character superscript subscript
'0' : ('\u2070', '\u2080' ),
'1' : ('\u00B9', '\u2081' ),
'2' : ('\u00B2', '\u2082' ),
'3' : ('\u00B3', '\u2083' ),
'4' : ('\u2074', '\u2084' ),
'5' : ('\u2075', '\u2085' ),
'6' : ('\u2076', '\u2086' ),
'7' : ('\u2077', '\u2087' ),
'8' : ('\u2078', '\u2088' ),
'9' : ('\u2079', '\u2089' ),
'a' : ('\u1d43', '\u2090' ),
'b' : ('\u1d47', '?' ),
'c' : ('\u1d9c', '?' ),
'd' : ('\u1d48', '?' ),
'e' : ('\u1d49', '\u2091' ),
'f' : ('\u1da0', '?' ),
'g' : ('\u1d4d', '?' ),
'h' : ('\u02b0', '\u2095' ),
'i' : ('\u2071', '\u1d62' ),
'j' : ('\u02b2', '\u2c7c' ),
'k' : ('\u1d4f', '\u2096' ),
'l' : ('\u02e1', '\u2097' ),
'm' : ('\u1d50', '\u2098' ),
'n' : ('\u207f', '\u2099' ),
'o' : ('\u1d52', '\u2092' ),
'p' : ('\u1d56', '\u209a' ),
'q' : ('?', '?' ),
'r' : ('\u02b3', '\u1d63' ),
's' : ('\u02e2', '\u209b' ),
't' : ('\u1d57', '\u209c' ),
'u' : ('\u1d58', '\u1d64' ),
'v' : ('\u1d5b', '\u1d65' ),
'w' : ('\u02b7', '?' ),
'x' : ('\u02e3', '\u2093' ),
'y' : ('\u02b8', '?' ),
'z' : ('?', '?' ),
'A' : ('\u1d2c', '?' ),
'B' : ('\u1d2e', '?' ),
'C' : ('?', '?' ),
'D' : ('\u1d30', '?' ),
'E' : ('\u1d31', '?' ),
'F' : ('?', '?' ),
'G' : ('\u1d33', '?' ),
'H' : ('\u1d34', '?' ),
'I' : ('\u1d35', '?' ),
'J' : ('\u1d36', '?' ),
'K' : ('\u1d37', '?' ),
'L' : ('\u1d38', '?' ),
'M' : ('\u1d39', '?' ),
'N' : ('\u1d3a', '?' ),
'O' : ('\u1d3c', '?' ),
'P' : ('\u1d3e', '?' ),
'Q' : ('?', '?' ),
'R' : ('\u1d3f', '?' ),
'S' : ('?', '?' ),
'T' : ('\u1d40', '?' ),
'U' : ('\u1d41', '?' ),
'V' : ('\u2c7d', '?' ),
'W' : ('\u1d42', '?' ),
'X' : ('?', '?' ),
'Y' : ('?', '?' ),
'Z' : ('?', '?' ),
'+' : ('\u207A', '\u208A' ),
'-' : ('\u207B', '\u208B' ),
'=' : ('\u207C', '\u208C' ),
'(' : ('\u207D', '\u208D' ),
')' : ('\u207E', '\u208E' ),
':alpha' : ('\u1d45', '?' ),
':beta' : ('\u1d5d', '\u1d66' ),
':gamma' : ('\u1d5e', '\u1d67' ),
':delta' : ('\u1d5f', '?' ),
':epsilon' : ('\u1d4b', '?' ),
':theta' : ('\u1dbf', '?' ),
':iota' : ('\u1da5', '?' ),
':pho' : ('?', '\u1d68' ),
':phi' : ('\u1db2', '?' ),
':psi' : ('\u1d60', '\u1d69' ),
':chi' : ('\u1d61', '\u1d6a' ),
':coffee' : ('\u2615', '\u2615' )
}
  • The keys in the dictionary are regular characters, such as letters, digits, and some special symbols.
  • The values associated with each key are tuples that contain two Unicode characters. The first character in the tuple represents the superscript version of the character, and the second character represents the subscript version of the character.

For example, let’s look at one entry in the dictionary:


'a': ('\u1d43', '\u2090'),
  • The key 'a' corresponds to the digit 'a.
  • The tuple ('\u1d43', '\u2090') contains two Unicode characters:
  • '\u1d43' represents the superscript 'a' (ᵃ).
  • '\u2080' represents the subscript 'a' (ₐ).

You can use this dictionary in Flutter.

With this dictionary, you can easily convert characters into their superscript or subscript forms by looking up the characters in the dictionary and choosing the appropriate Unicode character.

For example, if you wanted to convert the letter ‘a’ into its subscript form, you would use unicode_map['a'][1], which would give you '\u2090', representing the subscript 'a' (ₐ).

This dictionary can be useful when you need to programmatically generate text with subscript and superscript characters, such as in mathematical equations or scientific notations. It simplifies the process of working with these specialized characters in your code.

To use the unicode_map dictionary in Flutter, you can create a function or utility class that takes a regular text string and converts it to a string containing Unicode characters for subscript or superscript characters based on the mappings in the dictionary. Here's an example of how you can achieve this:

import 'package:flutter/material.dart';

void main() {
runApp(MyApp());
}

// Define the Unicode mapping dictionary
final Map<String, Tuple2<String, String>> unicodeMap = {
// ... (your Unicode mappings here)
};

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Example usage:
final subscriptText = getSubscriptText('H2O');
final superscriptText = getSuperscriptText('x2');

return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Unicode Map Example'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(subscriptText, style: TextStyle(fontSize: 24)),
Text(superscriptText, style: TextStyle(fontSize: 24)),
],
),
),
),
);
}

String getSubscriptText(String input) {
// Function to convert input text to subscript using the Unicode mapping
final result = input.split('').map((char) {
final unicode = unicodeMap[char];
return unicode != null ? unicode.item2 : char;
}).join('');
return result;
}

String getSuperscriptText(String input) {
// Function to convert input text to superscript using the Unicode mapping
final result = input.split('').map((char) {
final unicode = unicodeMap[char];
return unicode != null ? unicode.item1 : char;
}).join('');
return result;
}
}

In this example:

  1. We define the unicodeMap dictionary with your Unicode mappings for subscript and superscript characters.
  2. We create two functions, getSubscriptText and getSuperscriptText, that takes an input text and converts it to subscript or superscript text by looking up each character in the unicodeMap dictionary.
  3. We use these functions to convert text and display it in a Flutter app.

When you run this code, it will display the input text as subscript and superscript text based on the mappings defined in the unicodeMap dictionary. You can customize the mappings in the dictionary to include characters that you need for your specific use case.

--

--

Anna Muzykina

WTM Ambassador #Flutter developer #Mobile App Dev #Google Cloud Innovator #Mentor #Teacher