top of page
Search

The Text Expression Selector is arguably the most elusive Adobe After Effects Feature and yet it's its most powerful Text Feature.

  • Writer: Roland Kahlenberg
    Roland Kahlenberg
  • Apr 26
  • 13 min read

Updated: May 6

NOTE - I've updated the TWO Expressions at the end - more readable variable names and added Case-Sensitive Checkbox for Word/Phrase matching/selection.



This blog aims to provide insights and samples into the mystical world of Text Expression Selectors. Readers are assumed to know how to commit to basic application and adjustments using the Text Tool.



Text Expression Selectors are like mysterious orbs in the sky that everyone sees but only few speak about.





With the Range and Wiggly Selectors, we take note of two critical observations -


(1) what gets selected is quite easy to manage and


(2) how each selection is affected by Text Animator Properties is a lot trickier to manage


And it is the latter where the Text Expression Selector shines.





Before we proceed, let's define Text Objects.


Text Object : A group of one or more text characters defined by the Based On Parameter. The Based On Drop Menu provides 4 Group Types - Characters, Characters Excluding Spaces, Words and Lines.


You may also create your own Text Objects using any one of these as the basis for grouping. You may create a Text Object based on a sentence, a paragraph or a set of delimiters.





With Range Selectors, how each Text Object is affected and how it animates involves a complex formula comprising of various parameters in the Range Selector and Advanced Sections of a Text Animator Group.



With the Expression Selector, it's actually a lot easier to manage how Text Objects are affected by Text Animator Properties. The tricky part is you have to write your own Selection and Animation Functions. You willl want to start off with the default Text Expression Selector expression and then search for others that may be provided for free or as a paid product.





What you will want to take away from this blog is that Text Expression Selectors allow for more control over how each Text Object is affected by Text Animator Properties.





Let's take a look at a few Text Expression Selector examples because showing by example is always a good way for visual stuff.



What I'd like you to take away from watching this video is to appreciate how Text Objects are selected and animated with Expression Selectors and how the results here differ from results obtained with using Range and Wiggly Selectors.



Then, for each example, how do you think its Text Object is defined? There are no right or wrong answers but I want you to start looking at text animations with a more nuanced look to try to understand what's happening - what are the mechanics involved. Other than being able to visualize an end-result, you must also be able to define which type of Text Object you will use.





To make things more exciting for you; you can combine multiple Text Objects within a single Text Animator Group and even multiple Text Animator Groups, each with its own set of multiple Text Objects and with each driven by multiple Expression Selectors. Things can get really wild if you allow it to.





Text Expression Selector Samples deployed using AeXpression Notepad, a soon-to-be-released After Effects Script UI.



To add some brain work into the mix, pause at each example to ask yourself how you would re-create it using the Range and/or Wiggly Selectors.


Chances are you will not be able to replicate most, if any of these Text Animations if you used Range and/or Wiggly Selectors. And with this, the motivation is for you to start venturing into the world of Text Expression Selectors, if you have not done so.





Samples of After Effects Expression Text Selector. These and many others will be released either together with AeXpressions NotePad or shortly thereafter.




Text Selectors in After Effects



Most After Effects users are familiar with its Text Tool and its Range and Wiggly Selectors. However, only a tiny percentage have used the Text Expression Selector.



Advanced After Effects users will have used Expressions in their Range and Wiggly Selectors and even the Source Text Property. Now, let's take a look at the Text Expression Selector.



Text Expression Selector



Default Text Expression Selector

When you first apply the Text Expression Selector to a Text Animator Group, this is the expression that is applied –



selectorValue * textIndex/textTotal



Unfortunately, for almost every user that sees this for the first time, it's a cryptic line of code. So, let's try to clear this up - what you see are essentially three reserved variables; they are functions built into the After Effects Expression Engine.



Before we proceed, let's define these variables/functions - selectorValue * textIndex/textTotal


selectorValue: This is defined as a percentage of the value of the Text Animator Property you apply to the Animator Group.



The selectorValue * ratio is calculated for each Text Object on every frame. I use two different ranges for the selectorValue : 0 and 100 or -100 and 100. They provide different results and you should experiment further to appreciate the available nuances. As you deep dive further, you will realize you may have to set wider ranges to affect Text Objects differently.



What you should realize is that since everything is largely expression-driven, you can set your own ranges. So, keep an open mind and adjust your ranges to fit your end-goals.



textTotal: This is the total number of Text Objects based on the Based On Parameter.




textIndex: This defines the index of a single Text Object regardless of the type of Text Object selected with the Based On Drop Menu. Each Text Object receives a distinct index.



At each frame, the Expression Engine calculates the value each Text Object adopts from the Animator Property based on the ratio, textIndex/textTotal.


This gives us the default expression - selectorValue * textIndex/textTotal



Another way to explain how each Text Object receives its value is as follows -



Each Text Objects's value is calculated based on the ratio of the Animator Property Value to textTotal, multiplied by its textIndex.



This gives us the following equation which is mathematically equivalent to the default expression -


  selectorValue/textTotal * textIndex



The benefit of writing the formula this way is to isolate, textIndex.


With textIndex isolated, you will more easily appreciate selectorValue and textTotal as fixed values and that the textIndex function contains multiple mapped values, one for each Text Object.



To prove this hypothesis, set textIndex to a fixed value, like 5 or 10 and you will see all Text Objects will have the same exact value.





So, knowing the following, Text Objects, selectorValur and textIndex - how each is defined and their internal mechanics (for textIndex) you are now in a good position to understand and visualize how Animator Property values are mapped across the input text.





Now, let's proceed with some hands-on work by first setting up the group work



** Leave the Based On Parameter at its default selection, Characters.


** Disable the Range Selector by clicking on its Visibility Icon.


** Input the following as your Text Layer's Input String -

1234567890


** Set the Text Layer's Paragraph Justification to Left Aligned



** Apply the following Expression to the Text Layer's Anchor Point. This Expression places the Anchor Point at the bottom-left of the Text Layer.


const myLayerRect = thisLayer.sourceRectAtTime();

[myLayerRect.left , myLayerRect.top + myLayerRect.height]




The last two procedures are applied only to ensure we have identical results - they have no bearing on results directly related to Text Selector Expression.


If we have a future blog on the Text Expression Selector, we will look at combining Range and Expression Selectors - for now, let's focus on foundational topics.




With the default Text Expression Selector applied, add the Position Text Animator Property and set its xValue to 100 -> [100,0].





You will notice that the first Text Object is offset by 10 pixels and subsequent Text Objects are moved by 10 pixels the index of the Text Object. So, the Text Object with index 1 is offset by 10 pixels and the Text Object with index 2 gets offset by 20 pixels. And the last Text Object which has an index of 10 is offset by 10 pixels 10.



Let's compare the Before & After ...



Default Text Input String without a Text Animator Group.

The width of the Text Layer is 414 pixels at its default - no Text Selector is active.
The width of the Text Layer is 414 pixels at its default - no Text Selector is active.



Expression Selector applied with Text Animator Position Property set to [100,0 ].

The width of the Text Layer is 504 pixels with the Text Expression Selector active and the Position Text Animator Property set to [100,0].
The width of the Text Layer is 504 pixels with the Text Expression Selector active and the Position Text Animator Property set to [100,0].



Interesting result and a casual but useful observation ...


So, the default Text Selector Expression applies the selectorValue of the Text Animator Property to Text Objects much like what the Range Selector does when its Shape Parameter is set to Ramp Up and its Offset value is set to 0.



Range Selector Expression with Text Animator Position Property set to [100,0] – Shape Type set to Ramp Up.

The width of the Text Layer is 504 pixels with the Text Expression Selector disabled and we use the Range Selector with its Shape Type set to Ramp Up and its Offset set to 0. The Position Text Animator Property is set to [100,0].
The width of the Text Layer is 504 pixels with the Text Expression Selector disabled and we use the Range Selector with its Shape Type set to Ramp Up and its Offset set to 0. The Position Text Animator Property is set to [100,0].


Let's return focus to the Text Expression Selector ...



Default Text Expression Selector

selectorValue * textIndex/textTotal


Alternative way to write the formula -

selectorValue/textTotal * textIndex





Take note that the default expression applied to the Text Expression Selector does not animate anything - it only maps Text Animator Property values.


What the default expression does is distribute the selectorValue across the input text, weighted by each Text Object's Index – smaller indices receive a smaller weighting (lower value) and Text Objects with larger indices receive a higher weighting (higher value).




To animate Text Objects, we animate at least one of the three variables/functions in the default expression.




Additionally, textTotal should not be 0 as this will result in an expression error - a divide by 0 error. Other than this restriction, you should be able to experiment with different static and keyframed values to better understand the mechanics of the Text Expression Selector.



A couple of Helper Notes

Link each of the three variable/functions to a Slider Control and experiment by changing the value of one Slider Control at a time.


Then, keyframe or set non-animating values for each variable/function.




Key Takeaway ...


What I'd like you to keep an eye and your mind about is how you are able to control the value that is applied to Text Objects and the selection of Text Objects – they are independent when using the Text Expression Selector. This allows you to have greater control over what gets selected and and how they animate. Contrast with with using the Range Selector where it is a lot difficult to control how Text Objects animate.




If there is interest, I will dive deeper in a future Blog Post. I will also be launching a set of Text Expression Selectors. These will be distributed either as Animation Presets (FFX) or applied as Text Expression Selectors within an AEP.





Below are tutorials by three brilliant minds that should motivate you to take a further look into Text Expression Selectors.


At the end, I share two Text Expression Selector expressions tat perform the following tasks -


** select Letters/Numbers of your Input String


** select one or more words/phrases of your Input String





This tutorial is a good introduction and it's got a few examples you can easily apply to your work.


Text Animator's Expression Selector Explained

by ruthlessly quick AE tips







Joe of Workbench does an interesting introduction. Coupled with what I've written, this tutorial should provide more insights into use-cases for the Expression Text Selector.


Tutorial 34: Expression Selector Intro

by Workbench








Before diving into Luis' tutorial, you should dowload and take a look at his Expression to get an idea of what it does. His technique is excellent but he combines the use of the Text Expression Selector in ways which WILL confuse you if you are starting out in this area and you are not familiar with his Expression.



After Effects Rigging - Expression Selector

by Luis Martínez







Ilir takes matters into his own world. His Text Expression Selector Tutorials are quite extraordinary. Ilir has a few tutorials on the Expression Text Selector and YOU SHOULD watch all of them because they will blow your mind.


Advanced AE Expressions - Multiline Text Borders - Part 1 -

by Ilir Beqiri





Select single letter/number Expression

  • includes Case-Sensitive checkbox



This Expression is applied to the Amount Property of the Text Expression Selector.

This Expression allows you to select Individual Text Characters based on your input.

You can specify the character and its instance/occurrence in the Input Text String.


Each entry is "letter:occurrence". If no occurrence is specified, all instances of that Letter/Number are selected. Use a comma to separate different search Letters/Numbers.



Sample Use-Case ...

Search Array - "A:1", "T", "o" with Case-sensitive Checkbox enabled. Matches first instance of "A" and all instances of "o" and "T".


Search Array - "A:1", "T", "o" with Case-sensitive Checkbox enabled. Matches first instance of "A" and all instances of "o" and "T".
Search Array - "A:1", "T", "o" with Case-sensitive Checkbox enabled. Matches first instance of "A" and all instances of "o" and "T".



// START OF EXPRESSION


/* This Expression is applied to the Amount Property of a Text Expression Selector.


This Expression allows you to select word(s)/phrases.


** You can specify the character and its instance/occurrence in the Input Text String.


** Partial Word Match is supported via a Checkbox.


** By default, the Expression ignores punctuation marks that follow the last word of a matched word/phrase.


*/


// ** === SET BASED ON TO Characters ===



// 1. Read the Case-Sensitive checkbox (1 = on, 0 = off)

var caseSensitiveFlag = effect("CaseSensitive")("ADBE Checkbox Control-0001");


// 2. Define your search terms here

// e.g. ["A:1", "b", "o"]

var searchTerms = [

"A:1", // first “a” (upper or lower) when case-sensitive is off

"b", // all “b”s

"o" // all “o”s

];


// 3. Flatten the source text into an array of characters (ignore line breaks)

var originalString = text.sourceText.toString();

var flattenedChars = [];

for (var i = 0; i < originalString.length; i++) {

var thisChar = originalString.charAt(i);

if (thisChar !== "\r" && thisChar !== "\n") {

flattenedChars.push(thisChar);

}

}

var totalChars = flattenedChars.length;


// 4. Helper to compare one character against a search term

function charactersMatch(sourceChar, searchChar) {

if (caseSensitiveFlag === 1) {

// exact match

return sourceChar === searchChar;

} else {

// ignore case

return sourceChar.toLowerCase() === searchChar.toLowerCase();

}

}


// 5. Gather all matched character indices

var matchedPositions = [];


for (var t = 0; t < searchTerms.length; t++) {

var term = searchTerms[t].trim();

if (term === "") {

continue;

}

var parts = term.split(":");

var targetChar = parts[0];

if (parts.length > 1 && parts[1].trim() !== "") {

// Specific occurrence (e.g. "a:2")

var desiredOccurrence = parseInt(parts[1], 10);

var occurrenceCount = 0;

for (var j = 0; j < totalChars; j++) {

if (charactersMatch(flattenedChars[j], targetChar)) {

occurrenceCount++;

if (occurrenceCount === desiredOccurrence) {

matchedPositions.push(j);

break;

}

}

}

} else {

// All occurrences (e.g. "b")

for (var j = 0; j < totalChars; j++) {

if (charactersMatch(flattenedChars[j], targetChar)) {

matchedPositions.push(j);

}

}

}

}


// 6. Convert After Effects’ 1-based textIndex to a 0-based index

var zeroBasedTextIndex = textIndex - 1;


// 7. Return 100 if this character is in our matched list, otherwise 0

(matchedPositions.indexOf(zeroBasedTextIndex) !== -1) ? 100 : 0;



// END OF EXPRESSION

// Copyright 2025 - Roland Kahlenberg


/*

For personal and professional use. Not to be sold, resold, exchanged, shared on personal or publicly accessible sites.

.*/




Select word/phrases Text Selector Expression

  • includes Partial Word Match Checkbox




Sample Use-Case ...

Search Array - "Effect", "practical advice".

PartialWordMatch - unchecked



Sample Use-Case ...

Search Array - "Effect", "practical advice".

PartialWordMatch - checked




// START OF EXPRESSION


/* This Expression is applied to the Amount Property of the Text Expression Selector.


This Expression allows you to select word(s)/phrases.


** You can specify the character and its instance/occurrence in the Input Text String.


** Partial Word Match is supported via a Checkbox.


** By default, the Expression ignores punctuation marks that follow the last word of a matched word/phrase.


** SET BASED ON TO Characters Excluding Spaces

** Literal Word Selection


*/


// ** SET BASED ON TO Characters Excluding Spaces



/*

This Expression selects individual characters inside words that match your SearchArray entries.

It now supports:

• CaseSensitive checkbox

• PartialWordMatch checkbox

• Optional Nth-instance matching

• Excluding trailing punctuation from matches

*/


// 1. Read expression controls

var caseSensitiveFlag = effect("CaseSensitive") ("ADBE Checkbox Control-0001");

var partialWordMatchFlag = effect("PartialWordMatch")("ADBE Checkbox Control-0001");

var excludePunctuationFlag = 1; // set to 0 if you want to include trailing punctuation in your matches.


// 2. Define your search terms here:

// Each entry is "phrase" or "phrase:N", e.g. ["Effect","Note:2"]

var searchTerms = [

"Effect",

"Note:2"

];


// 3. Split source text into lines and then into an array of words

var sourceText = text.sourceText.toString();

var lines = sourceText.split("\n");

var allWords = [];

for (var i = 0; i < lines.length; i++) {

var L = lines[i].trim();

if (!L) continue;

allWords = allWords.concat(L.split(/\s+/));

}


// 4. Helper functions for matching

function stripPunct(w) {

return excludePunctuationFlag

? w.replace(/[.,!?;:]$/, "")

: w;

}

function equalsWord(src, tgt) {

if (!caseSensitiveFlag) {

src = src.toLowerCase();

tgt = tgt.toLowerCase();

}

return src === tgt;

}

function containsWord(src, tgt) {

if (!caseSensitiveFlag) {

src = src.toLowerCase();

tgt = tgt.toLowerCase();

}

return src.indexOf(tgt) >= 0;

}


// 5. Find all matching word indices and metadata

var matchedWordIndices = [];

var matchMetadata = [];


for (var s = 0; s < searchTerms.length; s++) {

var entry = searchTerms[s].trim();

if (!entry) continue;

var parts = entry.split(":");

var phrase = parts[0];

var wantNth = parts.length > 1 && parts[1].trim() ? parseInt(parts[1], 10) : null;

if (wantNth !== null && (isNaN(wantNth) || wantNth < 1)) wantNth = null;

var targetWords = phrase.split(/\s+/);

var lastIdx = targetWords.length - 1;

var trailingP = /[.,!?;:]$/.test(targetWords[lastIdx]);

// Clean punctuation off last target word if needed

for (var tw = 0; tw < targetWords.length; tw++) {

if (excludePunctuationFlag && !trailingP && tw === lastIdx) {

targetWords[tw] = targetWords[tw].replace(/[.,!?;:]$/, "");

}

}

var matchCount = 0;

// scan through allWords looking for phrase matches

for (var w = 0; w <= allWords.length - targetWords.length; w++) {

var ok = true;

for (var tw = 0; tw < targetWords.length; tw++) {

var srcWord = stripPunct(allWords[w + tw]);

var tgtWord = targetWords[tw];

if (partialWordMatchFlag) {

if (!containsWord(srcWord, tgtWord)) { ok = false; break; }

} else {

if (!equalsWord(srcWord, tgtWord)) { ok = false; break; }

}

}

if (!ok) continue;

matchCount++;

if (wantNth === null || matchCount === wantNth) {

for (var tw = 0; tw < targetWords.length; tw++) {

var flatIdx = w + tw;

var srcWord = stripPunct(allWords[flatIdx]);

var tgtWord = targetWords[tw];

var offset = partialWordMatchFlag

? (caseSensitiveFlag

? srcWord.indexOf(tgtWord)

: srcWord.toLowerCase().indexOf(tgtWord.toLowerCase()))

: 0;

var length = partialWordMatchFlag

? tgtWord.length

: srcWord.length;

matchedWordIndices.push(flatIdx);

matchMetadata.push({

wordIndex: flatIdx,

matchOffset: offset,

matchLength: length

});

}

if (wantNth !== null) break;

}

}

}


// 6. Build character boundaries for each word

var boundaries = [];

var charPos = 1;

for (var ln = 0; ln < lines.length; ln++) {

var rawLine = lines[ln];

if (!rawLine.trim()) continue;

var wordsInLine = rawLine.split(/\s+/);

for (var wi = 0; wi < wordsInLine.length; wi++) {

var wText = wordsInLine[wi];

var startChar = charPos;

charPos += wText.length;

var endChar = charPos - 1;

var searchFrom = boundaries.length

? boundaries[boundaries.length-1].flatIndex + 1

: 0;

var flatIndex = allWords.indexOf(wText, searchFrom);

boundaries.push({

flatIndex: flatIndex,

startChar: startChar,

endChar: endChar

});

}

if (ln < lines.length - 1 && lines[ln+1].trim()) {

charPos++;

}

}


// 7. Test if current textIndex falls inside any matched substring

var zeroBasedIdx = textIndex;

var isMatch = false;

for (var b = 0; b < boundaries.length; b++) {

var bd = boundaries[b];

var mi = matchedWordIndices.indexOf(bd.flatIndex);

if (mi < 0) continue;

var md = matchMetadata[mi];

var sChar = bd.startChar + md.matchOffset;

var eChar = sChar + md.matchLength - 1;

if (zeroBasedIdx >= sChar && zeroBasedIdx <= eChar) {

isMatch = true;

break;

}

}


// 8. Return selector value

isMatch ? 100 : 0;



// END OF EXPRESSION

// Copyright 2025 - Roland Kahlenberg


/*

For personal and professional use. Not to be sold, resold, exchanged, shared on personal or publicly accessible sites. */




 
 
 

Comments


bottom of page