Suffix
suffix places a small marker immediately after the visible content. Choose a terminal-style block cursor or a softly pulsing dot. The marker follows the text through paragraphs, lists, inline formatting, tables, and code blocks.
Marker styleCursor Dot Custom Content typeParagraph Inline code Code block List Table
The marker stays visible after playback so you can inspect its position. In your app, set streaming to false to remove it.
Switch the content type to inspect different endings. Pause or replay the stream, or choose a custom type to see how the renderer handles it. The demo keeps its marker visible after playback so you can inspect the spacing.
Usage
Choose your framework above each example. Pass the Core example’s mappers to the Core inputs to enable the mappers explicitly. Components enable the same effects through streaming.effect and include their visual renderers.
Core React Vue Svelte Angular React Native Lynx Solid Taro
import type { MapperPluggable } from '@fluxdown/core' ;
import { Smooth } from '@fluxdown/core-plugins/plugins/mapper/smooth' ;
import { Suffix } from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
const mappers : MapperPluggable [] = [
[Smooth , { enabled : true }] ,
[Suffix , { enabled : true , type : 'cursor' }] ,
]; import { Fluxdown } from 'fluxdown' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
text = {text}
/>
);
} < script setup lang = "ts" >
import { Fluxdown } from '@fluxdown/vue' ;
defineProps <{ text : string }>();
</ script >
< template >
< Fluxdown
: streaming = " { effect : [ 'suffix' , { type : 'cursor' }] } "
: text = " text "
/>
</ template > < script lang = "ts" >
import { Fluxdown } from '@fluxdown/svelte' ;
type MessageProps = {
text : string ;
};
let { text } : MessageProps = $ props ();
</ script >
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
{ text }
/> import type { StreamingConfig } from '@fluxdown/angular' ;
import { Component , input } from '@angular/core' ;
import { Fluxdown } from '@fluxdown/angular' ;
@ Component ({
selector : 'app-message' ,
imports : [Fluxdown] ,
template : `<fluxdown [streaming]="streaming" [text]="text()" />` ,
})
export class Message {
readonly text = input .required < string >();
readonly streaming : StreamingConfig = {
effect : [ 'suffix' , { type : 'cursor' }] ,
};
} import { Fluxdown } from '@fluxdown/react-native' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/lynx' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/solid' ;
type MessageProps = {
text : string ;
};
export function Message (props : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
text = { props .text}
/>
);
} import { Fluxdown } from '@fluxdown/taro' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'suffix' , { type : 'cursor' }] }}
text = {text}
/>
);
}
Use type: 'dot' for a circular marker. effect: 'suffix' uses the block cursor by default. Both shapes inherit the current text color and scale with the text. Their breathing animation respects prefers-reduced-motion.
The marker remains visible while the effect is enabled. Set streaming to false when the response finishes, or set the suffix's enabled option to false to keep other streaming behavior active:
Core React Vue Svelte Angular React Native Lynx Solid Taro
import type { MapperPluggable } from '@fluxdown/core' ;
import { Smooth } from '@fluxdown/core-plugins/plugins/mapper/smooth' ;
import { Suffix } from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { MutableState , toClosure } from 'stative' ;
const isStreaming = MutableState .of ( true );
const active = toClosure (isStreaming);
const mappers : MapperPluggable [] = [
[Smooth , { enabled : true }] ,
[Suffix , { enabled : active , type : 'dot' }] ,
]; import { Fluxdown } from 'fluxdown' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
export function Message ({ text , isStreaming } : MessageProps ) {
return (
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
}}
text = {text}
/>
);
} < script setup lang = "ts" >
import { Fluxdown } from '@fluxdown/vue' ;
defineProps <{ text : string ; isStreaming : boolean }>();
</ script >
< template >
< Fluxdown
: streaming = " {
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
} "
: text = " text "
/>
</ template > < script lang = "ts" >
import { Fluxdown } from '@fluxdown/svelte' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
let { text , isStreaming } : MessageProps = $ props ();
</ script >
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
}}
{ text }
/> import type { StreamingConfig } from '@fluxdown/angular' ;
import { Component , input , computed } from '@angular/core' ;
import { Fluxdown } from '@fluxdown/angular' ;
@ Component ({
selector : 'app-message' ,
imports : [Fluxdown] ,
template : `<fluxdown [streaming]="streaming()" [text]="text()" />` ,
})
export class Message {
readonly text = input .required < string >();
readonly isStreaming = input ( false );
readonly streaming = computed < StreamingConfig >(() => {
return {
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : this .isStreaming () }] ,
};
});
} import { Fluxdown } from '@fluxdown/react-native' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
export function Message ({ text , isStreaming } : MessageProps ) {
return (
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
}}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/lynx' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
export function Message ({ text , isStreaming } : MessageProps ) {
return (
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
}}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/solid' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
export function Message (props : MessageProps ) {
return (
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : props .isStreaming }] ,
}}
text = { props .text}
/>
);
} import { Fluxdown } from '@fluxdown/taro' ;
type MessageProps = {
text : string ;
isStreaming : boolean ;
};
export function Message ({ text , isStreaming } : MessageProps ) {
return (
< Fluxdown
streaming = {{
smooth : true ,
effect : [ 'suffix' , { type : 'dot' , enabled : isStreaming }] ,
}}
text = {text}
/>
);
}
Combine the marker with an entrance effect:
Core React Vue Svelte Angular React Native Lynx Solid Taro
import type { MapperPluggable } from '@fluxdown/core' ;
import { Smooth } from '@fluxdown/core-plugins/plugins/mapper/smooth' ;
import { Scale } from '@fluxdown/core-plugins/plugins/mapper/scale' ;
import { Suffix } from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
const mappers : MapperPluggable [] = [
[Smooth , { enabled : true }] ,
[Scale , { enabled : true }] ,
[Suffix , { enabled : true , type : 'dot' }] ,
]; import { Fluxdown } from 'fluxdown' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
text = {text}
/>
);
} < script setup lang = "ts" >
import { Fluxdown } from '@fluxdown/vue' ;
defineProps <{ text : string }>();
</ script >
< template >
< Fluxdown
: streaming = " { effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] } "
: text = " text "
/>
</ template > < script lang = "ts" >
import { Fluxdown } from '@fluxdown/svelte' ;
type MessageProps = {
text : string ;
};
let { text } : MessageProps = $ props ();
</ script >
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
{ text }
/> import type { StreamingConfig } from '@fluxdown/angular' ;
import { Component , input } from '@angular/core' ;
import { Fluxdown } from '@fluxdown/angular' ;
@ Component ({
selector : 'app-message' ,
imports : [Fluxdown] ,
template : `<fluxdown [streaming]="streaming" [text]="text()" />` ,
})
export class Message {
readonly text = input .required < string >();
readonly streaming : StreamingConfig = {
effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] ,
};
} import { Fluxdown } from '@fluxdown/react-native' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/lynx' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
text = {text}
/>
);
} import { Fluxdown } from '@fluxdown/solid' ;
type MessageProps = {
text : string ;
};
export function Message (props : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
text = { props .text}
/>
);
} import { Fluxdown } from '@fluxdown/taro' ;
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
streaming = {{ effect : [ 'scale' , [ 'suffix' , { type : 'dot' }]] }}
text = {text}
/>
);
}
Render a custom type
The type option is 'cursor' | 'dot' | (string & {}). Every value uses the same HAST node: a span whose dataFluxdownSuffix property contains that type. The built-in renderer recognizes only cursor and dot; it returns null for every other value. An unknown type is metadata for your renderer, not text to display.
Use a render plugin for your framework to display a smile. SuffixRenderer shares the built-in marker layout and breathing animation. Vue uses h, Svelte uses createElement, and Lynx keeps its JSX rendering function outside the plugin class:
React Vue Svelte Angular React Native Lynx Solid Taro
import type {
ReactRenderMatchParams ,
ReactRenderParams ,
} from '@fluxdown/react-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseReactRenderPlugin } from '@fluxdown/react-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/react-plugins/plugins/effect-suffix' ;
import { Fluxdown } from 'fluxdown' ;
class SmileRenderPlugin extends BaseReactRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : ReactRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : ReactRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return < SuffixRenderer type = "smile" >🙂</ SuffixRenderer >;
}
}
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
plugins = {{ renders : [SmileRenderPlugin] }}
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
text = {text}
/>
);
} < script setup lang = "ts" >
import type {
VueRenderMatchParams ,
VueRenderParams ,
} from '@fluxdown/vue-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseVueRenderPlugin } from '@fluxdown/vue-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/vue-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/vue' ;
import { h } from 'vue' ;
class SmileRenderPlugin extends BaseVueRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : VueRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : VueRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return h (SuffixRenderer , { type : 'smile' , children : '🙂' });
}
}
const plugins = { renders : [SmileRenderPlugin] };
defineProps <{ text : string }>();
</ script >
< template >
< Fluxdown
: plugins = " plugins "
: streaming = " { effect : [ 'suffix' , { type : 'smile' }] } "
: text = " text "
/>
</ template > < script lang = "ts" >
import type {
SvelteRenderMatchParams ,
SvelteRenderParams ,
} from '@fluxdown/svelte-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseSvelteRenderPlugin , createElement } from '@fluxdown/svelte-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/svelte-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/svelte' ;
class SmileRenderPlugin extends BaseSvelteRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : SvelteRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : SvelteRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return createElement (SuffixRenderer , { type : 'smile' } , '🙂' );
}
}
const plugins = { renders : [SmileRenderPlugin] };
type MessageProps = {
text : string ;
};
let { text } : MessageProps = $ props ();
</ script >
< Fluxdown
{ plugins }
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
{ text }
/> import type {
AngularRenderMatchParams ,
AngularRenderParams ,
} from '@fluxdown/angular-plugins/typings' ;
import type { StreamingConfig } from '@fluxdown/angular' ;
import { Component , input } from '@angular/core' ;
import {
BaseAngularRenderPlugin ,
createComponentNode ,
} from '@fluxdown/angular-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/angular-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/angular' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
class SmileRenderPlugin extends BaseAngularRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : AngularRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : AngularRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return createComponentNode (
SuffixRenderer ,
{ Raw : null , type : 'smile' } ,
{ tagName : 'span' , children : '🙂' } ,
);
}
}
@ Component ({
selector : 'app-message' ,
imports : [Fluxdown] ,
template : `<fluxdown
[plugins]="plugins"
[streaming]="streaming"
[text]="text()"
/>` ,
})
export class Message {
readonly text = input .required < string >();
readonly plugins = { renders : [SmileRenderPlugin] };
readonly streaming : StreamingConfig = {
effect : [ 'suffix' , { type : 'smile' }] ,
};
} import type {
ReactRenderMatchParams ,
ReactRenderParams ,
} from '@fluxdown/react-native-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseReactRenderPlugin } from '@fluxdown/react-native-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/react-native-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/react-native' ;
class SmileRenderPlugin extends BaseReactRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : ReactRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : ReactRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return (
< SuffixRenderer Raw = { null } type = "smile" >
🙂
</ SuffixRenderer >
);
}
}
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
plugins = {{ renders : [SmileRenderPlugin] }}
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
text = {text}
/>
);
} import type {
LynxRenderMatchParams ,
LynxRenderParams ,
} from '@fluxdown/lynx-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseLynxRenderPlugin } from '@fluxdown/lynx-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/lynx-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/lynx' ;
function renderSmile ({ node } : LynxRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return < SuffixRenderer type = "smile" >🙂</ SuffixRenderer >;
}
class SmileRenderPlugin extends BaseLynxRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : LynxRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
readonly render = renderSmile;
}
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
plugins = {{ renders : [SmileRenderPlugin] }}
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
text = {text}
/>
);
} import type {
SolidRenderMatchParams ,
SolidRenderParams ,
} from '@fluxdown/solid-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseSolidRenderPlugin } from '@fluxdown/solid-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/solid-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/solid' ;
import { Show } from 'solid-js' ;
class SmileRenderPlugin extends BaseSolidRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : SolidRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render (props : SolidRenderParams ) {
return (
< Show
when = {
isSuffixNode ( props .node) &&
props . node .properties[ SUFFIX_DATA_ATTR ] === 'smile'
}
>
< SuffixRenderer type = "smile" >🙂</ SuffixRenderer >
</ Show >
);
}
}
type MessageProps = {
text : string ;
};
export function Message (props : MessageProps ) {
return (
< Fluxdown
plugins = {{ renders : [SmileRenderPlugin] }}
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
text = { props .text}
/>
);
} import type {
ReactRenderMatchParams ,
ReactRenderParams ,
} from '@fluxdown/taro-plugins/base' ;
import {
isSuffixNode ,
SUFFIX_DATA_ATTR ,
} from '@fluxdown/core-plugins/plugins/mapper/suffix' ;
import { BaseReactRenderPlugin } from '@fluxdown/taro-plugins/base' ;
import { SuffixRenderer } from '@fluxdown/taro-plugins/plugins/effect-suffix' ;
import { Fluxdown } from '@fluxdown/taro' ;
class SmileRenderPlugin extends BaseReactRenderPlugin {
static readonly key = 'render-smile' ;
match ({ node } : ReactRenderMatchParams ) {
return isSuffixNode (node) && node .properties[ SUFFIX_DATA_ATTR ] === 'smile' ;
}
render ({ node } : ReactRenderParams ) {
if ( ! isSuffixNode (node) || node .properties[ SUFFIX_DATA_ATTR ] !== 'smile' ) {
return null ;
}
return < SuffixRenderer type = "smile" >🙂</ SuffixRenderer >;
}
}
type MessageProps = {
text : string ;
};
export function Message ({ text } : MessageProps ) {
return (
< Fluxdown
plugins = {{ renders : [SmileRenderPlugin] }}
streaming = {{ effect : [ 'suffix' , { type : 'smile' }] }}
text = {text}
/>
);
}
At the same priority, the supplied renderer handles smile before the preset renderer. Other types continue to use the presets. The demo uses this same plugin and animation.
The reactive Core example owns its MutableState: call isStreaming.next(false) when receiving finishes, then dispose active and isStreaming with .destroy() after destroying Core.
Code blocks and Core
Inside a code block, the marker follows the visible code, on the same line as its last character. An explicit trailing blank line is preserved, so its marker appears on that blank line. It stays separate from the source code, so copying the block does not include the marker. Custom code slots should render their suffix prop after the highlighted code content to preserve this behavior.
Core's Suffix mapper inserts the marker at the tail of the visible HAST, without adding text length. Your renderer decides how to display it.
The Suffix mapper, SuffixConfig, SuffixType, SUFFIX_DATA_ATTR, and isSuffixNode are exported from @fluxdown/core-plugins/plugins/mapper/suffix.
Configuration
These fields belong to the suffix configuration object in ['suffix', config].
enabled Optional
Type: boolean
Default: true
Show the suffix at the end of the current visible content. Disable it when generation finishes, or set streaming to false to disable all streaming behavior.
type Optional
Type: 'cursor' | 'dot' | (string & {})
Default: 'cursor'
Choose a block cursor or circular dot. Every type is stored on the same HAST marker node; the default renderer returns null for unrecognized types.