• English
  • 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.

    Follow the end of the content

    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.

    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' }],
    ];

    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:

    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' }],
    ];

    Combine the marker with an entrance effect:

    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' }],
    ];

    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:

    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}
        />
      );
    }

    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.