UI Customizations

JoinStories SDK offers different ways to display your widget. There are currently two available triggers:

  • Bubbles
  • Cards

BubbleWidget

  • alias: required identifier; not a UI customization.
PropertyTypeDefaultDescription
controllerBubbleController?nullEnables refresh() without rebuilding
configurationBubbleConfiguration?nullVisual/behavior customization (see below)

BubbleConfiguration

PropertyTypeDefaultAllowed valuesDescription
showLabelbooltrueShow label under each bubble
labelStyleTextStyle?nullLabel fontSize, fontWeight, fontFamily
labelColorColor?nullLabel color override
thumbViewSpacingdouble12.0Space between bubbles
thumbViewSizedouble74.0Bubble diameter (px)
loaderColorsList<Color>[Color(0xFFD9222D), Color(0xFFFFA800)]Loader gradient colors
loaderWidthdouble2.0Loader stroke width
storyViewedIndicatorColorColor?nullColor for viewed indicator ring
showPlayButtonbooltrueShow play icon overlay
animationTypeBubbleAnimationTypenonenone, pulseFirst bubble animation
fontNameString?nullPlatform font override
labelFontSizedouble?nullLabel font size override
maxStoriesint?nullMax items (−1 for all)
customParametersMap<String, String>?nullAudience targeting params
horizontalMargindouble?nullOuter horizontal margin
horizontalPaddingdouble?nullInner horizontal padding
reorderedReadStoriesbool?nullMove read stories
playerBackgroundColorColor?nullPlayer background color
playerVerticalAnchorString?nulltop, center, bottomPlayer vertical alignment
playerHorizontalMarginsdouble?nullPlayer horizontal margins
playerCornerRadiusdouble?nullPlayer corner radius
playerProgressBarDefaultColorColor?nullProgress bar base color
playerProgressBarFillColorColor?nullProgress bar fill color
playerProgressBarThicknessdouble?nullProgress bar thickness
playerProgressBarRadiusdouble?nullProgress bar corner radius
playerStandaloneAnimationOriginString?nulltop, topLeft, topRight, bottom, bottomLeft, bottomRightStandalone open animation origin

Example

import 'package:join_stories_flutter/join_stories_flutter.dart';

BubbleWidget(
  alias: 'widget-alias',
  configuration: const BubbleConfiguration(
    showLabel: true,
    labelColor: Color(0xFF2C3E50),
    labelFontSize: 14,
    fontName: 'Avenir',
    showPlayButton: true,
    thumbViewSpacing: 8,
    thumbViewSize: 100,
    storyViewedIndicatorColor: Color(0xFFE74C3C),
    loaderColors: [Color(0xFF3498DB), Color(0xFF2ECC71)],
    loaderWidth: 3,
    animationType: BubbleAnimationType.pulse,
    reorderedReadStories: true,
    maxStories: 5,
    horizontalMargin: 16,
    horizontalPadding: 8,

    // Player (standalone) styling used when opening the player from this widget
    playerBackgroundColor: Color(0xFF2C3E50),
    playerVerticalAnchor: 'center',
    playerHorizontalMargins: 20.0,
    playerCornerRadius: 12.0,
    playerProgressBarDefaultColor: Color(0xFF7F8C8D),
    playerProgressBarFillColor: Color(0xFF3498DB),
    playerProgressBarThickness: 4.0,
    playerProgressBarRadius: 8.0,
    playerStandaloneAnimationOrigin: 'top',
  ),

  // Trigger + Player + Analytics callbacks
  onTriggerFetchSuccess: (count) {},
  onTriggerFetchEmpty: () {},
  onTriggerFetchError: (message) {},
  onPlayerFetchSuccess: () {},
  onPlayerLoaded: () {},
  onPlayerFetchError: (message) {},
  onPlayerDismissed: (type) {}, // 'auto' | 'manual'
  onContentLinkClick: (link) {},
  onAnalyticsEvent: (name, payload) {},
);

CardWidget

  • alias: required identifier; not a UI customization.
PropertyTypeDefaultAllowed valuesDescription
isGridboolfalseGrid when true; list when false
controllerCardController?nullEnables refresh() without rebuilding
configurationCardConfiguration?nullVisual/behavior customization (see below)

CardConfiguration

PropertyTypeDefaultAllowed valuesDescription
cardRadiusdouble8.0Card corner radius
cardElevationdouble2.0Card elevation
showPlayButtonbooltruePlay icon overlay
showLabelbooltrueShow label text
labelStyleTextStyle?nullLabel fontSize, fontWeight, fontFamily
labelColorColor?nullLabel color override
showOverlaybooltrueBottom overlay visibility
spacingdouble6.0Space between cards
borderColorColor?nullCard border color
borderWidthint0Card border width
storyViewedBorderColorColor?nullBorder color when viewed
numberOfColumnsint?nullGrid columns (grid mode only)
cardSizedouble?nullFixed size; grid=list height, list=width
horizontalMargindouble0.0Outer horizontal margin
horizontalPaddingdouble0.0Inner horizontal padding
animationTypeCardAnimationTypenonenone, pulseFirst card animation
fontNameString?nullPlatform font override
labelFontSizedouble?nullLabel font size override
maxStoriesint?nullMax items (−1 for all)
customParametersMap<String, String>?nullAudience targeting params
reorderedReadStoriesbool?nullMove read stories
playerBackgroundColorColor?nullPlayer background color
playerVerticalAnchorString?nulltop, center, bottomPlayer vertical alignment
playerHorizontalMarginsdouble?nullPlayer horizontal margins
playerCornerRadiusdouble?nullPlayer corner radius
playerProgressBarDefaultColorColor?nullProgress bar base color
playerProgressBarFillColorColor?nullProgress bar fill color
playerProgressBarThicknessdouble?nullProgress bar thickness
playerProgressBarRadiusdouble?nullProgress bar corner radius
playerStandaloneAnimationOriginString?nulltop, topLeft, topRight, bottom, bottomLeft, bottomRightStandalone open animation origin

Example

// Card List
CardListWidget(
  alias: 'widget-alias',
  configuration: const CardConfiguration(
    showLabel: true,
    labelColor: Color(0xFF8E44AD),
    labelFontSize: 16,
    fontName: 'Avenir',
    showPlayButton: true,
    cardElevation: 4,
    cardRadius: 12,
    showOverlay: true,
    spacing: 12,
    borderWidth: 2,
    borderColor: Color(0xFF9B59B6),
    storyViewedBorderColor: Color(0xFFE67E22),
    animationType: CardAnimationType.pulse,
    reorderedReadStories: false,
    maxStories: 3,

    // Optional player styling when launching from the card
    playerBackgroundColor: Color(0xFF8E44AD),
    playerVerticalAnchor: 'bottom',
    playerHorizontalMargins: 15.0,
    playerCornerRadius: 8.0,
    playerProgressBarDefaultColor: Color(0xFF95A5A6),
    playerProgressBarFillColor: Color(0xFFE74C3C),
    playerProgressBarThickness: 3.0,
    playerProgressBarRadius: 6.0,
    playerStandaloneAnimationOrigin: 'bottomRight',
  ),
  // same callbacks as BubbleWidget (triggers, player, links, analytics)
);

// Grid Card
CardGridWidget(
  alias: 'widget-alias',
  configuration: const CardConfiguration(
    showLabel: true,
    labelColor: Color(0xFF27AE60),
    labelFontSize: 18,
    fontName: 'Avenir',
    showPlayButton: true,
    cardElevation: 6,
    cardRadius: 16,
    showOverlay: true,
    spacing: 16,
    numberOfColumns: 2,
    cardSize: 150,
    borderWidth: 3,
    borderColor: Color(0xFF2ECC71),
    storyViewedBorderColor: Color(0xFFF39C12),
    animationType: CardAnimationType.pulse,
    reorderedReadStories: true,
    maxStories: 5,
  ),
);

Standalone player

Configuration

ParameterTypeDefaultAllowed valuesDescription
aliasStringWidget alias to load
standaloneOriginString?nulltop, topLeft, topRight, bottom, bottomLeft, bottomRightOpen animation origin
playerBackgroundColorint?nullARGB background color
playerVerticalAnchorString?nulltop, center, bottomVertical alignment
playerHorizontalMarginsdouble?nullHorizontal margins
playerCornerRadiusdouble?nullCorner radius
playerProgressBarDefaultColorint?nullProgress bar base color (ARGB)
playerProgressBarFillColorint?nullProgress bar fill color (ARGB)
playerProgressBarThicknessdouble?nullProgress bar thickness
playerProgressBarRadiusdouble?nullProgress bar corner radius
  • Optional callbacks are also supported on JOINStories.startPlayer(...) (e.g., onPlayerLoaded, onPlayerDismissed, analytics and ecommerce callbacks), but they are event hooks rather than visual customizations.

  • Provided complete customization tables for BubbleWidget, CardWidget, and the standalone player, with types, defaults, and allowed values.

Example

await JOINStories.startPlayer(
  'widget-alias',
  standaloneOrigin: 'top', // 'top'|'topLeft'|'topRight'|'bottom'|'bottomLeft'|'bottomRight'
  playerBackgroundColor: 0xFF2C3E50,
  playerVerticalAnchor: 'center',
  playerHorizontalMargins: 20.0,
  playerCornerRadius: 12.0,
  playerProgressBarDefaultColor: 0xFF7F8C8D,
  playerProgressBarFillColor: 0xFF3498DB,
  playerProgressBarThickness: 4.0,
  playerProgressBarRadius: 8.0,
  onPlayerFetchSuccess: () {},
  onPlayerLoaded: () {},
  onPlayerFetchError: (msg) {},
  onPlayerDismissed: (type) {},  // 'auto' | 'manual'
  onContentLinkClick: (link) {},
  onAnalyticsEvent: (eventName, payload) {},
);

Custom Fonts

JOIN widgets (Bubble/Card) are native views. To ensure a custom font applies inside those native views, make it available both to Flutter and the native side.

Flutter steps (standard UI)

  • Add .ttf/.otf files under assets/fonts/.
  • Declare them in pubspec.yaml (family = name used by Flutter UI):
flutter:
  uses-material-design: true
  fonts:
    - family: Parisienne
      fonts:
        - asset: assets/fonts/Parisienne-Regular.ttf
  • Optional: apply globally in your Flutter app:
MaterialApp(
  theme: ThemeData(fontFamily: 'Parisienne'),
)

iOS (required for JOIN widgets)

  • Copy the font into your iOS project, e.g. ios/Runner/Fonts/Parisienne-Regular.ttf (ensure Target Membership to Runner).
  • Add it to ios/Runner/Info.plist:
<key>UIAppFonts</key>
<array>
  <string>Fonts/Parisienne-Regular.ttf</string>
  <!-- add other variants if needed -->
</array>
  • Use the exact PostScript name in the JOIN configurations (fontName), for example:
BubbleWidget(
  alias: '...',
  configuration: const BubbleConfiguration(
    fontName: 'Parisienne-Regular',
  ),
)

CardListWidget(
  alias: '...',
  configuration: const CardConfiguration(
    fontName: 'Parisienne-Regular',
  ),
)

Notes:

  • If the font doesn’t apply, verify the PostScript name (e.g. with macOS Font Book) and cold restart the app.

Android

  • Either:
    • a) Put the font under android/app/src/main/res/font/ (e.g. parisienne_regular.ttf), optionally define a res/font/\*.xml family.
    • b) Keep the font in Flutter assets (assets/fonts/...). The Android bridge attempts to load from flutter_assets/... and assets/fonts/... and falls back to res/font or a system font.
  • For JOIN, pass the same fontName as iOS (e.g. 'Parisienne-Regular').

Best practices:

  • Only add variants you actually use (Regular/Bold/Italic). fontName selects a specific variant; there’s no automatic weight/style mapping.
  • Always cold restart after adding/modifying fonts.