Skip to content

Themes#

Use StorytellerTheme to apply your brand's colors, typography, spacing, and controls to Storyteller UI. Start with the practical setup below, then use the property reference when you need a specific setting.

Configuring a StorytellerTheme#

A StorytellerTheme contains separate light and dark Theme values. The component's uiStyle selects which one is rendered:

uiStyle Theme used
.light StorytellerTheme.light
.dark StorytellerTheme.dark
.auto The current system appearance
Omitted Keeps the view's current setting. A newly created view starts at .auto.

Choose the scope before you customize the theme:

Scope How to set it Use it when
Global Storyteller.shared.theme Most or all Storyteller UI should use the same brand theme.
Component The theme value in a list or player configuration One component needs a deliberate variation from the global theme.

Set the global theme before configuring Storyteller views, normally alongside your app's Storyteller initialization. StorytellerTheme is a value type, so if you change your local copy later, assign it to Storyteller.shared.theme again and reconfigure any existing component that should use the new values.

Creating Themes#

Start with a small group of visible brand choices. Configure both appearances explicitly rather than assuming that a light value is also suitable in dark mode.

import StorytellerSDK
import UIKit

var brandTheme = StorytellerTheme()

brandTheme.light.colors.primary = UIColor(red: 0.04, green: 0.31, blue: 0.76, alpha: 1)
brandTheme.light.lists.backgroundColor = UIColor.white

brandTheme.dark.colors.primary = UIColor(red: 0.36, green: 0.66, blue: 1, alpha: 1)
brandTheme.dark.lists.backgroundColor = UIColor(red: 0.06, green: 0.07, blue: 0.09, alpha: 1)

Storyteller.shared.theme = brandTheme

The primary color is inherited by supported accent surfaces such as unread circular-tile indicators. The list background changes with the selected appearance. Once this works, add only the properties your design requires.

Verify light and dark appearances#

During development, temporarily force each appearance with uiStyle. This separates theme configuration problems from the simulator or device's current appearance.

import StorytellerSDK

let darkPreviewRow = StorytellerStoriesRowView()
darkPreviewRow.configure(with: StorytellerStoriesListConfiguration(
    categories: ["category-id"],
    uiStyle: .dark
))

Verify the same integration with .light, then explicitly return a reused view to .auto when the app should follow the system appearance. Omitting uiStyle does not reset an existing view; it leaves that view's current setting unchanged.

Apply a theme to one component#

A configuration-level theme replaces the global host theme for that component; it is not merged over Storyteller.shared.theme. Start from a copy of the global theme when you want a small variation.

import StorytellerSDK
import UIKit

var compactRowTheme = Storyteller.shared.theme
compactRowTheme.light.colors.primary = UIColor.systemOrange
compactRowTheme.light.lists.row.tileSpacing = 4
compactRowTheme.dark.colors.primary = UIColor.systemOrange
compactRowTheme.dark.lists.row.tileSpacing = 4

let storytellerStoriesRow = StorytellerStoriesRowView()
storytellerStoriesRow.configure(with: StorytellerStoriesListConfiguration(
    categories: ["category-id"],
    theme: compactRowTheme,
    uiStyle: .auto
))

This row uses compactRowTheme. When the SDK opens the Player from this list, the Player uses the same override. Other independently configured components continue to use the global theme. See StorytellerListView for the full list configuration contract.

Configure custom fonts#

Custom fonts require setup in the host app as well as a StorytellerFontProvider:

  1. Add each .ttf or .otf file to the app target and confirm its target membership.
  2. Add each filename to Fonts provided by application (UIAppFonts) in the app's Info settings. For a manually maintained Info.plist, the equivalent entry is:

    <key>UIAppFonts</key>
    <array>
        <string>AcmeSans-Light.otf</string>
        <string>AcmeSans-Regular.otf</string>
        <string>AcmeSans-Medium.otf</string>
        <string>AcmeSans-Semibold.otf</string>
        <string>AcmeSans-Bold.otf</string>
    </array>
    
  3. Use the font's internal PostScript name in UIFont(name:size:). This is not always the same as the filename or the name displayed in Finder.

  4. Map every StorytellerFontWeight requested by the SDK and assign the provider to both appearances.
import StorytellerSDK
import UIKit

final class BrandFontProvider: StorytellerFontProvider, @unchecked Sendable {
    override func font(weight: StorytellerFontWeight, size: CGFloat) -> UIFont? {
        let postScriptName: String

        switch weight {
        case .light:
            postScriptName = "AcmeSans-Light"
        case .regular:
            postScriptName = "AcmeSans-Regular"
        case .medium:
            postScriptName = "AcmeSans-Medium"
        case .semibold:
            postScriptName = "AcmeSans-Semibold"
        case .bold, .heavy, .black:
            postScriptName = "AcmeSans-Bold"
        @unknown default:
            postScriptName = "AcmeSans-Regular"
        }

        return UIFont(name: postScriptName, size: size)
    }
}

let brandFont = BrandFontProvider()
for weight in StorytellerFontWeight.allCases {
    assert(brandFont.font(weight: weight, size: 16) != nil)
}

var fontTheme = StorytellerTheme()
fontTheme.light.customFont = brandFont
fontTheme.dark.customFont = brandFont
Storyteller.shared.theme = fontTheme

The assertion is a useful development check: if it fails, confirm the asset's target membership, UIAppFonts filename, and PostScript name. The SDK's default StorytellerFontProvider supplies system fonts. A custom provider can return nil, but individual UI surfaces may then retain or choose their own fallback; return a font for every weight if consistent typography is required.

StorytellerFontWeight includes .light. An exhaustive switch written against an older SDK must add .light or an @unknown default branch when it is recompiled with SDK 11.5.1 or later.

Configure remote rectangular tile title fonts#

Storyteller Settings can select the font family and weight used by titles on rectangular Story and Clip tiles independently from the global host theme:

theme.light.tiles.rectangularTile.title.fontFamily
theme.light.tiles.rectangularTile.title.fontWeight
theme.dark.tiles.rectangularTile.title.fontFamily
theme.dark.tiles.rectangularTile.title.fontWeight

These are remote Settings values, not public Theme properties. They apply to rectangular title-bearing tiles in rows, grids, category screens, and Search. They do not change circular tiles, eyebrows, list headings, or Player text.

The host app must include and register every requested font face before the SDK renders a tile. Follow the target-membership and UIAppFonts steps in Configure custom fonts. Set fontFamily to the registered UIKit family name, such as Acme Sans; this differs from the PostScript face name such as AcmeSans-Semibold used with UIFont(name:size:). During development, use UIFont.familyNames to verify the registered family spelling.

fontWeight is a numeric string. The iOS SDK supports these mappings:

Remote value StorytellerFontWeight
"300" .light
"400" .regular
"500" .medium
"600" .semibold
"700" .bold
"800" .heavy
"900" .black

Family and weight inherit independently. A feed- or Search-specific leaf takes precedence over its tenant-level value. A missing, null, blank, malformed, or unknown leaf keeps the corresponding tenant value when present. After remote inheritance, an absent weight keeps the existing .heavy rectangular-title weight. An absent or unregistered family asks the selected host customFont provider for the resolved weight; the SDK's default provider returns the matching system font. If a custom provider cannot create the resolved weight, the SDK retries the existing .heavy rectangular-title weight before allowing the surface's normal font fallback.

Configure rectangular tile title gradients#

Rectangular Story and Clip tiles can use an ordered, multi-stop gradient behind their titles in rows, grids, category screens, and Search. Circular tiles and Player gradients are unaffected.

For a host-supplied theme, assign a stop-based Theme.Gradient to tiles.rectangularTile.titleGradient in each appearance that needs it:

import StorytellerSDK
import UIKit

var tileTheme = StorytellerTheme()

tileTheme.light.tiles.rectangularTile.titleGradient = Theme.Gradient(
    stops: [
        Theme.Gradient.Stop(color: UIColor.clear, location: 0),
        Theme.Gradient.Stop(color: UIColor.black.withAlphaComponent(0.25), location: 0.55),
        Theme.Gradient.Stop(color: UIColor.black.withAlphaComponent(0.72), location: 1),
    ],
    startPosition: .topCenter,
    endPosition: .bottomCenter
)

tileTheme.dark.tiles.rectangularTile.titleGradient =
    tileTheme.light.tiles.rectangularTile.titleGradient

A valid stop gradient contains at least two stops. Locations must be finite, within 0...1, and in non-decreasing order. Repeated colors and locations are valid, and the first and last locations do not need to be 0 and 1. Invalid host gradients keep the existing iOS tile-title gradient.

Storyteller Settings can provide the same capability under theme.light.tiles.rectangularTile.titleGradient and theme.dark.tiles.rectangularTile.titleGradient:

{
  "theme": {
    "light": {
      "tiles": {
        "rectangularTile": {
          "titleGradient": {
            "stops": [
              { "color": "#00000000", "location": 0 },
              { "color": "#00000000", "location": 0.55 },
              { "color": "#000000B8", "location": 1 }
            ],
            "startPosition": "topCenter",
            "endPosition": "bottomCenter"
          }
        }
      }
    }
  }
}

Stop colors accept #RRGGBB or alpha-last #RRGGBBAA. The stop list is validated atomically: a malformed color or location rejects that remote gradient without discarding valid sibling theme fields. Missing, incomplete, equal, or unsupported remote positions use topCenter to bottomCenter.

The value resolves in this order: feed-specific remote appearance, tenant-level remote appearance, selected host light/dark theme, then the existing iOS default. The default remains clear at 0, black at 50% opacity at 0.45, and black at 80% opacity at 1, from top center to bottom center.

Existing two-color configurations remain supported when stops is absent or null. Their remote colors retain the legacy #RRGGBB or alpha-first #AARRGGBB format:

{
  "titleGradient": {
    "startColor": "#00000000",
    "endColor": "#CC000000",
    "startPosition": "topCenter",
    "endPosition": "bottomCenter"
  }
}

Configure Sheet corner radius#

Use sheets.cornerRadius to set the top-corner radius, in points, for Storyteller content Sheets and the Search Filters sheet. Configure light and dark appearances independently:

var theme = StorytellerTheme()

theme.light.sheets.cornerRadius = 12
theme.dark.sheets.cornerRadius = 12

Storyteller.shared.theme = theme

The equivalent remote Settings fragment is:

{
  "theme": {
    "light": {
      "sheets": {
        "cornerRadius": 12
      }
    },
    "dark": {
      "sheets": {
        "cornerRadius": 12
      }
    }
  }
}

A feed- or collection-specific remote value takes precedence over the tenant-level remote value, which takes precedence over the selected host theme. Sheet actions opened from a Story or Clip use that presentation's active theme context. Card, direct, and deeplink Sheet opens use the global theme context.

0 requests square top corners. Any finite value greater than or equal to zero is accepted; negative, non-finite, and malformed values are ignored so the next valid layer can apply. When no layer supplies a valid value, Search Filters retain their existing 6-point radius and content Sheets retain UIKit's system radius. The content-Sheet override is available on iOS 15 and newer; iOS 13 and 14 always keep the system presentation radius.

Understand precedence and fallback#

Theme resolution follows this order:

  1. uiStyle chooses the light or dark appearance.
  2. A theme supplied in the component configuration is used for that component; otherwise the SDK uses Storyteller.shared.theme.
  3. The SDK fills unset optional properties from related theme values or its own defaults. For example, an unset circular-tile unread indicator inherits colors.primary.
  4. Where a property supports remote theming, a feed- or collection-specific remote value takes precedence over a tenant-level remote value, which in turn takes precedence over the selected host theme value.

Remote configuration does not cover every public Theme property. If a host value is unexpectedly replaced, check whether that property is configured for the current feed or tenant before changing the app theme.

Common recipes#

Use the narrowest property that expresses the design change:

  • Change the main accent: set colors.primary. Dependent defaults such as unread indicators inherit it unless you override them directly.
  • Tighten a horizontal row: set lists.row.tileSpacing, lists.row.startInset, and lists.row.endInset in both appearances.
  • Style general buttons: set buttons.backgroundColor, buttons.textColor, and buttons.cornerRadius.
  • Style Storyteller Sheets: set sheets.cornerRadius in both appearances; see Configure Sheet corner radius for defaults and platform availability.
  • Style the instruction start button: use instructions.button.backgroundColor and instructions.button.textColor; these detailed instruction values apply to the iOS instruction overlay.
  • Improve Clips Player readability: configure player.clips.topGradient and player.clips.bottomGradient; see Gradient and Player for supported values and defaults.

The Showcase app keeps a larger real-world theme in one place. See StorytellerThemeManager.globalTheme for colors, fonts, list styling, and item-specific variations.

Verify and troubleshoot#

Check one visible value at a time before applying a complete design system:

Symptom Check
No component changes Assign the theme before configuring the view. If you mutated a local copy after assignment, assign it again and reconfigure the component.
One component looks different Inspect the theme passed in that component's configuration; it replaces the global host theme for that component.
Only one appearance is correct Force .light and .dark in turn and confirm that both branches contain the intended values.
The system font still appears Confirm target membership, UIAppFonts, PostScript names, and a non-nil result for every requested weight.
A remote rectangular title font does not appear Confirm the remote fontFamily matches a value in UIFont.familyNames, every required face is registered, and the active light/dark branch contains the expected numeric-string weight.
Only some surfaces change Confirm that the property applies to that surface and whether a more specific property or supported remote value takes precedence.

For a wider diagnosis flow, see Appearance or Configuration Does Not Change.

Property reference#

The remainder of this page documents the public host Theme properties. Optional properties often inherit from the palette, primitives, or another theme property; each table identifies that fallback where applicable. Values managed through Storyteller Settings are identified separately and are not host Theme properties.

Colors#

The colors property on theme is used to establish a set of base colors for the SDK to use.

Property Default Value Data Type Description
primary #1C62EB UIColor The default accent color used throughout the UI. In general, this should be the primary brand color.
success #3BB327 UIColor Used to indicate correct answers in Quizzes.
alert #E21219 UIColor Used to indicate incorrect answers in Quizzes.
white.primary #FFFFFF UIColor Used for white text
white.secondary white.primary at 85% opacity UIColor Used for light text
white.tertiary white.primary at 70% opacity UIColor Used for gray text
black.primary #1A1A1A UIColor Used for dark text and backgrounds
black.secondary black.primary at 85% opacity UIColor Used for light black text
black.tertiary black.primary at 70% opacity UIColor Used for gray text

Font#

Property Default Value Data Type Description
customFont System font matching the requested weight and size StorytellerFontProvider Supplies fonts throughout Storyteller UI. Assign it separately to the light and dark themes.

See Configure custom fonts for asset registration, PostScript-name verification, and a complete provider example.


Primitives#

The primitives object contains base values which are used throughout the UI.

Property Default Value Data Type Description
cornerRadius 8 CGFloat The corner radius used for rectangular tiles, buttons and poll/quiz answers

Lists#

The lists customizes properties of the various list types available from the SDK.

Property Default Value Data Type Description
backgroundColor colors.white.primary in light mode; colors.black.primary in dark mode UIColor? Background color for Storyteller lists.
enablePlayerOpen true Bool Controls whether the SDK opens the player when a tile is tapped. When set to false, the SDK will not open the player and the app must handle tile taps via StorytellerListViewDelegate.onTileTapped(data:) in UIKit or the .onTileTapped(data:) list action in SwiftUI. See onTileTapped. This setting is not applied for lists on SDK‑owned screens (Storyteller Home, Followable Categories, and Search) where the SDK always opens the player.
animateTilesOnReorder true Bool When the reloadData() method is called to update lists, a reorder animation is added to visualise the updating process.
row.tileSpacing 8 CGFloat The space between each Tile in a row
row.startInset 12 CGFloat The space before the first Tile in a row
row.endInset 12 CGFloat The space after the last Tile in a row
grid.tileSpacing 8 CGFloat The space between each Tile in a grid, both vertically and horizontally
grid.columns 2 Int The number of columns in a grid. Not applicable to Search and Category screens.
grid.topInset 0 CGFloat The space before the first row in a grid
grid.bottomInset 0 CGFloat The space after the last row in a grid
title.font customFont StorytellerFontProvider? Defines the font of the Title in Section
title.textSize 22 CGFloat Size of the Title in Section
title.lineHeight 28 CGFloat The line height of the Title in Section
title.textCase default StorytellerTextCasing Sets the text case for the Title in Section. Possible values are upper, lower and default
title.textColor nil UIColor Color of Title in Section

Gradient#

The Gradient struct supports the existing two-color initializer and an ordered stop-based initializer. Stop gradients are used by rectangular tile titles; existing two-color integrations remain source- and decode-compatible.

Property Default Value Data Type Description
startColor Required UIColor The color where the gradient begins.
endColor Required UIColor The color where the gradient ends.
startPosition Required Theme.Gradient.GradientPosition The position indicating where the gradient starts.
endPosition Required Theme.Gradient.GradientPosition The position indicating where the gradient ends.
stops nil for the two-color initializer [Theme.Gradient.Stop]? Ordered colors and locations supplied by the stop-based initializer.

Each Theme.Gradient.Stop contains a UIColor and a CGFloat location.

Enum: GradientPosition#

Defines positions for starting and ending points of the gradient.

Value Description
bottomLeft Bottom left corner of the gradient area.
bottomCenter Bottom center edge of the gradient area.
bottomRight Bottom right corner of the gradient area.
centerLeft Center left edge of the gradient area.
centerCenter Center of the gradient area.
centerRight Center right edge of the gradient area.
topLeft Top left corner of the gradient area.
topCenter Top center edge of the gradient area.
topRight Top right corner of the gradient area.

GradientPosition is a nested enum, not an Int-backed value. Construct a gradient by supplying all four required values:

import StorytellerSDK
import UIKit

let brandGradient = Theme.Gradient(
    startColor: UIColor.systemBlue,
    endColor: UIColor.systemPurple,
    startPosition: .topCenter,
    endPosition: .bottomCenter
)

Tiles#

The tiles property can be used to customize the appearance of the Tiles.

Property Default Value Data Type Description
chip.textSize 11 CGFloat Text size for the New Indicator and Live Indicator.
chip.show true Bool Used to show/hide the new/live chip
title.textSize 11 CGFloat Size of the Title on a Tile
title.lineHeight 13 CGFloat The line height of the Title on a Tile
title.alignment center StorytellerAlignment The alignment of the Title on a Tile. Possible values are left, center and right
circularTile.title.unreadTextColor inherits colors.black.primary for light, colors.white.primary for dark UIColor The text color of the Title for a circular tile when the story or the clip is unread
circularTile.title.readTextColor inherits colors.black.tertiary for light, colors.white.tertiary for dark UIColor The text color of the Tile for a circular tile when the story or the clip is read
circularTile.unreadIndicatorColor inherits colors.primary UIColor The color of the ring around a circular tile when the story or the clip is unread
circularTile.readIndicatorColor #C5C5C5 UIColor The color of the ring around a circular tile when the story or the clip is read
circularTile.unreadIndicatorGradient nil Gradient? The gradient of the ring around a circular tile when the story or the clip is unread. If set, overrides circularTile.unreadIndicatorColor
circularTile.unreadIndicatorBorderColor nil UIColor? The border color of the ring around a circular tile when the story or the clip is unread
circularTile.readIndicatorBorderColor nil UIColor? The border color of the ring around a circular tile when the story or the clip is read
circularTile.unreadBorderWidth 2 CGFloat The width of Circular Tile ring border in unread state
circularTile.readBorderWidth 1 CGFloat The width of Circular Tile ring border in read state
circularTile.liveChip.readImage null UIImage? Image to be used in place of default read Live Indicator.
circularTile.liveChip.unreadImage null UIImage? Image to be used in place of default unread Live Indicator
circularTile.liveChip.unreadBackgroundColor colors.alert UIColor Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
circularTile.liveChip.readBackgroundColor colors.black.tertiary UIColor Background color of the Live Indicator when all story pages have been read or the clip has been viewed.
circularTile.liveChip.unreadBackgroundGradient nil Gradient? The gradient of the ring around a live tile and background of the Live Indicator. If set, overrides circularTile.liveChip.unreadBackgroundColor
circularTile.liveChip.unreadTextColor colors.white.primary UIColor Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
circularTile.liveChip.readTextColor colors.white.primary UIColor Text color of the Live Indicator when all story pages have been read or the clip has been viewed.
circularTile.liveChip.unreadBorderColor null UIColor? Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
circularTile.liveChip.readBorderColor null UIColor? Boder color of the Live Indicator when all story pages have been read or the clip has been viewed.
rectangularTile.padding 8 CGFloat The internal padding for a rectangular story or the clip tile
rectangularTile.title.textColor inherits colors.white.primary UIColor The text color of the Title for a rectangular tile
rectangularTile.titleGradient clear at 0, 50%-alpha black at 0.45, 80%-alpha black at 1 Gradient? The ordered gradient rendered behind rectangular Story and Clip tile titles. Feed and tenant remote values can override the selected host appearance.
rectangularTile.chip.alignment end StorytellerAlignment The alignment of the New Indicator and Live Indicator in Rectangular Tiles. Possible values are start, center or end
rectangularTile.unreadIndicator.image null UIImage? An image which can be used in place of the default unread indicator for a rectangular tile. If set, overrides rectangularTile.unreadIndicator.gradient
rectangularTile.unreadIndicator.gradient nil Gradient? The background gradient of the unread indicator for a rectangular tile. If set, overrides rectangularTile.unreadIndicator.backgroundColor
rectangularTile.unreadIndicator.backgroundColor inherits colors.primary UIColor The background color of the unread indicator for a rectangular tile
rectangularTile.unreadIndicator.textColor inherits colors.white.primary UIColor The text color of the unread indicator for a rectangular tile
rectangularTile.unreadIndicator.borderColor null UIColor? Border color of the unread indicator for a rectangular tile
rectangularTile.liveChip.readImage null UIImage? Image to be used in place of default read Live Indicator.
rectangularTile.liveChip.unreadImage null UIImage? Image to be used in place of default unread Live Indicator. If set, overrides rectangularTile.liveChip.unreadBackgroundGradient
rectangularTile.liveChip.unreadBackgroundGradient nil Gradient? Gradient background to be used for the Live Indicator. If set, overrides rectangularTile.liveChip.unreadBackgroundColor
rectangularTile.liveChip.unreadBackgroundColor colors.alert UIColor Background color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
rectangularTile.liveChip.readBackgroundColor colors.black.tertiary UIColor Background color of the Live Indicator when all pages have been read or the clip has been viewed
rectangularTile.liveChip.unreadTextColor colors.white.primary UIColor Text color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
rectangularTile.liveChip.readTextColor colors.white.primary UIColor Text color of the Live Indicator when all story pages have been read or the clip has been viewed.
rectangularTile.liveChip.unreadBorderColor null UIColor? Border color of the Live Indicator when the story contains unread pages or the clip has not been viewed.
rectangularTile.liveChip.readBorderColor null UIColor? Border color of the Live Indicator when all story pages have been read or the clip has been viewed.

Diagram illustrating rowTheme rectangular options

Diagram illustrating rowTheme circular options


Player#

The player property is used to customize properties relating to the Story and Clips Player.

Property Default Value Data Type Description
showStoryIcon false Bool Shows the round story icon before the Story Title in the Player
showTimestamp true Bool Shows the timestamp after the Story Title in the Player, indicating how long ago a story was published
showShareButton true Bool Shows the share button in the Player. Setting this to false entirely disables sharing in Storyteller
showLikeButton true Bool Shows the like button in the Clips Player. Setting this to false entirely disables liking in Storyteller
enableFollowableCategorySwipeFromRightEdge true Bool Enables opening the Followable Category screen with a right-edge swipe in the Clips Player. Set to false to disable this gesture
clips.showButtonBackgrounds true Bool Controls whether the Clips Player renders the default button backgrounds for like, share, mute, and captions buttons
clips.actionIconSize 32 CGFloat Size in points for Clips Player action icons. Values outside the supported 24...38 range are clamped during theme resolution
clips.feedSwitcher.selected.fontWeight selection-style default StorytellerFontWeight? Optional selected For You / Following tab weight. When set, it overrides the weight implied by theme.behavior.following.feedSwitcher.selectionStyle
clips.feedSwitcher.selected.textSize 16 CGFloat? Optional selected For You / Following tab text size
clips.feedSwitcher.selected.lineHeight nil CGFloat? Optional selected For You / Following tab line height
clips.feedSwitcher.unselected.fontWeight selection-style default StorytellerFontWeight? Optional unselected For You / Following tab weight. When set, it overrides the weight implied by theme.behavior.following.feedSwitcher.selectionStyle
clips.feedSwitcher.unselected.textSize 16 CGFloat? Optional unselected For You / Following tab text size
clips.feedSwitcher.unselected.lineHeight nil CGFloat? Optional unselected For You / Following tab line height
clips.eyebrow.font inherits theme.customFont StorytellerFontProvider? Font family used for the Clips Player eyebrow row. The SDK requests the .regular weight at render time
clips.eyebrow.textSize 15 CGFloat Text size used for the Clips Player eyebrow row
clips.eyebrow.lineHeight nil CGFloat? Optional line height for the Clips Player eyebrow row. nil preserves the current UIKit spacing for the resolved font
clips.eyebrow.textColor inherits theme.colors.white.secondary UIColor? Text color used for the Clips Player eyebrow row
clips.title.font inherits theme.customFont StorytellerFontProvider? Font family used for the Clips Player main title. The SDK requests clips.title.fontWeight, defaulting to .bold
clips.title.fontWeight .bold StorytellerFontWeight? Optional weight requested from the Clips Player title font provider
clips.title.textSize 16 CGFloat Text size used for the Clips Player main title
clips.title.lineHeight nil CGFloat? Optional line height for the Clips Player main title. nil preserves the system default line spacing for the resolved font
clips.title.textColor inherits theme.colors.white.primary UIColor? Text color used for the Clips Player main title
clips.categoryNavigation.fontWeight .bold StorytellerFontWeight? Optional category-navigation label weight requested from theme.customFont
clips.categoryNavigation.textSize 15 CGFloat? Optional category-navigation label text size
clips.categoryNavigation.lineHeight nil CGFloat? Optional category-navigation label line height
clips.topGradient black at 60% opacity from .topCenter to clear at .bottomCenter Theme.Gradient? Customizes the existing 96-point top readability scrim without changing its bounds or visibility
clips.bottomGradient clear at .topCenter to black at 60% opacity at .bottomCenter Theme.Gradient? Customizes the existing metadata-backed bottom readability scrim without changing its bounds or visibility rule
clips.modal nil Theme.Player.Clips.Modal? Optional host-configured layout for modal Clips, including status-bar video, internal spacing, gradient height, progress dimensions and navigation imagery. See Modal Clips layout
clips.embeddedVideoSizing nil (inherits remote Settings, then .legacy) StorytellerEmbeddedClipsVideoSizing? Opts Embedded Clips content and supported video Ads into full-width, aspect-preserving, vertically centred media sizing. It does not affect modal/fullscreen Clips, Stories, or tvOS
clips.progressBar.playedColor inherits colors.white.primary UIColor? Color of the completed portion of the Clips progress track
clips.progressBar.remainingColor white at 40% opacity UIColor? Color of the remaining portion of the Clips progress track
clips.progressBar.position .bottom Theme.Player.Clips.ProgressBar.Position Sets the Clips progress bar to .bottom or .aboveAction. .aboveAction applies only to Embedded Clips with a visible primary action
clips.progressBar.active.playedColor inherits clips.progressBar.playedColor UIColor? Completed-track color while the user is scrubbing
clips.progressBar.active.remainingColor inherits clips.progressBar.remainingColor UIColor? Remaining-track color while the user is scrubbing
clips.progressBar.active.currentTimeColor inherits colors.white.primary UIColor? Current-time label color while the user is scrubbing
clips.progressBar.active.totalTimeColor inherits colors.white.tertiary UIColor? Total-time label color while the user is scrubbing
clips.spacing.backButtonStartInset 0 CGFloat Left inset in points for the Clips Player back/close button
clips.spacing.contentInsetHorizontal 16 CGFloat Horizontal (start/end) padding in points for the description and action content areas
clips.spacing.contentInsetBottom 16 CGFloat Bottom padding in points for the description and action content areas
clips.spacing.actionSpacing 12 CGFloat Vertical spacing in points between action icons (follow, like, share, mute, caption)
clips.spacing.eyebrowToTitleSpacing 4 CGFloat Spacing in points between the eyebrow text and the clip title
clips.spacing.titleToActionSpacing 16 CGFloat Horizontal gap in points between the title/description text and the action icons column
clips.spacing.titleToCategoriesSpacing nil CGFloat? Vertical gap in points between the rendered title/description block and categories in Embedded Clips
clips.spacing.categoriesToMoreSpacing nil CGFloat? Vertical gap in points between categories and the shared expansion/collapse affordance in Embedded Clips
clips.spacing.metadataToProgressBarSpacing nil CGFloat? Minimum vertical gap in points between the metadata block and the visible progress track in Embedded Clips
clips.spacing.progressBarToActionSpacing nil CGFloat? Minimum vertical gap in points between the visible progress track and primary action in Embedded Clips, independent of progress-bar ordering
liveChip.image null UIImage? Image used in place of Live Chip before Live Story or Clip Titles. If set, it overrides liveChip.backgroundGradient
liveChip.textColor null UIColor? Text color used for badge label for Live Story or Clip
liveChip.backgroundGradient null Gradient? Background gradient of the badge for Live Story or Clip. If set, it overrides liveChip.backgroundColor
liveChip.backgroundColor theme.colors.alert UIColor? Background color of the badge for Live Story or Clip
liveChip.borderColor null UIColor Border color of the badge for Live Story or Clip
icons.share null UIImage? An image to be used in place of the default share icon
icons.refresh null UIImage? Refresh button image to be used in place of refresh share icon, used in the error state
icons.back null UIImage? Back button image to be used in place of the default Clips back icon
icons.like.initial null UIImage? An image to be used in place of the default like icon when the clip is not liked
icons.like.liked null UIImage? An image to be used in place of the default like icon when the clip is liked
icons.like.animation.liked null StorytellerPlayerIcons.LikeIcons.Animation.Resource? Bundled Lottie animation played when the Clips like button changes from unliked to liked
icons.like.animation.unliked null StorytellerPlayerIcons.LikeIcons.Animation.Resource? Bundled Lottie animation played when the Clips like button changes from liked to unliked
icons.mute.muted null UIImage? Image used for the mute button when audio is muted in Stories and Clips
icons.mute.unmuted null UIImage? Image used for the mute button when audio is unmuted in Stories and Clips
icons.captions.enabled null UIImage? Image used for the captions button when captions are enabled in Stories and Clips
icons.captions.disabled null UIImage? Image used for the captions button when captions are disabled in Stories and Clips

Like and share count visibility is managed through Storyteller Settings and is not exposed through the public host Theme API.

When a Clip includes a long description, the Player renders it below the title using customFont at semibold 15pt and colors.white.secondary. Title, long description, and categories each show up to two lines while collapsed. If any section overflows, one more/less control expands or collapses all three sections together. This behavior is automatic and does not add a theme property.

If only one mute or captions state is provided, the missing state continues to use the bundled Storyteller icon.

Example:

var theme = StorytellerTheme()

theme.light.player.clips.showButtonBackgrounds = false
theme.light.player.clips.actionIconSize = 24
theme.light.player.clips.eyebrow = .init(
    font: StorytellerFontProvider(),
    textSize: 20,
    lineHeight: 24,
    textColor: UIColor.white
)
theme.light.player.clips.title = .init(
    font: StorytellerFontProvider(),
    textSize: 20,
    lineHeight: 24,
    textColor: UIColor.white,
    fontWeight: .regular
)
theme.light.player.clips.feedSwitcher = .init(
    selected: .init(fontWeight: .regular, textSize: 15, lineHeight: 20),
    unselected: .init(fontWeight: .light, textSize: 15, lineHeight: 20)
)
theme.light.player.clips.categoryNavigation = .init(
    fontWeight: .regular,
    textSize: 13,
    lineHeight: 16
)
theme.light.player.clips.topGradient = .init(
    startColor: UIColor.systemBlue.withAlphaComponent(0.8),
    endColor: .clear,
    startPosition: .topLeft,
    endPosition: .bottomRight
)
theme.light.player.clips.bottomGradient = .init(
    startColor: UIColor.systemPink.withAlphaComponent(0.8),
    endColor: .clear,
    startPosition: .bottomRight,
    endPosition: .topLeft
)
theme.light.player.clips.progressBar.position = .aboveAction
theme.light.player.clips.spacing = .init(
    backButtonStartInset: 12,
    contentInsetHorizontal: 16,
    contentInsetBottom: 20,
    actionSpacing: 20,
    eyebrowToTitleSpacing: 6,
    titleToActionSpacing: 16,
    titleToCategoriesSpacing: 8,
    categoriesToMoreSpacing: 4,
    metadataToProgressBarSpacing: 16,
    progressBarToActionSpacing: 16
)
theme.light.player.icons = StorytellerPlayerIcons(
    back: UIImage(named: "icon-back-custom"),
    muteMuted: UIImage(named: "icon-mute-muted-custom"),
    muteUnmuted: UIImage(named: "icon-mute-unmuted-custom"),
    captionsEnabled: UIImage(named: "icon-captions-enabled-custom"),
    captionsDisabled: UIImage(named: "icon-captions-disabled-custom")
)

The canonical key path for this API is theme.player.clips.eyebrow.

clips.eyebrow.lineHeight intentionally defaults to nil to preserve the current UIKit eyebrow spacing.

clips.title.lineHeight intentionally defaults to nil, unlike other title theme APIs that ship a non-nil line height. Leaving it unset preserves the current Clips title spacing.

Clips player typography#

The feed switcher and category-navigation labels inherit theme.customFont; they do not have separate font-family properties. The clip title continues to use clips.title.font, falling back to theme.customFont. Each configured weight is requested from that provider.

When feed-switcher weights are absent, underline selection mode keeps .semibold for both tabs, while text-weight selection mode keeps .heavy selected and .medium unselected. Explicit selected or unselected weights replace only those fallback weights; theme.behavior.following.feedSwitcher.selectionStyle still controls underline visibility, animation, and the remaining selection behavior.

Configured sizes and line heights scale with Dynamic Type. Category names and the delimiter from theme.behavior.player.clips.categoryNavigation.delimiter share one attributed typography style, while the existing less control keeps its current style.

Host values and remote light/dark values resolve independently for every typography field. A feed- or collection-specific remote value overrides the tenant/global remote value, which overrides the host-supplied light or dark theme. Remote font-weight strings are case-insensitive, so values such as REGULAR, LIGHT, and BLACK resolve to the corresponding lowercase-backed public enum cases. Missing, unknown, non-finite, or non-positive remote values continue to the next fallback without changing compatibility output.

Sky configuration:

var theme = StorytellerTheme()

theme.light.player.clips.feedSwitcher.selected = .init(
    fontWeight: .regular,
    textSize: 15,
    lineHeight: 20
)
theme.light.player.clips.feedSwitcher.unselected = .init(
    fontWeight: .light,
    textSize: 15,
    lineHeight: 20
)
theme.light.player.clips.title.fontWeight = .regular
theme.light.player.clips.title.textSize = 15
theme.light.player.clips.title.lineHeight = 20
theme.light.player.clips.categoryNavigation = .init(
    fontWeight: .regular,
    textSize: 13,
    lineHeight: 16
)

Equivalent remote Settings fragment:

{
  "theme": {
    "light": {
      "player": {
        "clips": {
          "feedSwitcher": {
            "selected": { "fontWeight": "REGULAR", "textSize": 15, "lineHeight": 20 },
            "unselected": { "fontWeight": "LIGHT", "textSize": 15, "lineHeight": 20 }
          },
          "title": { "fontWeight": "REGULAR", "textSize": 15, "lineHeight": 20 },
          "categoryNavigation": { "fontWeight": "REGULAR", "textSize": 13, "lineHeight": 16 }
        }
      }
    },
    "dark": {
      "player": {
        "clips": {
          "feedSwitcher": {
            "selected": { "fontWeight": "REGULAR", "textSize": 15, "lineHeight": 20 },
            "unselected": { "fontWeight": "LIGHT", "textSize": 15, "lineHeight": 20 }
          },
          "title": { "fontWeight": "REGULAR", "textSize": 15, "lineHeight": 20 },
          "categoryNavigation": { "fontWeight": "REGULAR", "textSize": 13, "lineHeight": 16 }
        }
      }
    }
  }
}

Clips player gradients#

theme.player.clips.topGradient and theme.player.clips.bottomGradient use the existing two-colour Theme.Gradient type. All nine GradientPosition values are supported for either endpoint, and the start and end positions must be different.

When topGradient is absent at every layer, the Clips Player keeps its existing black-at-60%-opacity to clear gradient from .topCenter to .bottomCenter over the fixed 96-point top scrim. When bottomGradient is absent at every layer, it keeps the existing clear-at-top to black-at-60%-opacity-at-bottom appearance over the title-stack-derived bottom scrim. The bottom scrim remains hidden when the Clip title is missing or empty.

Each gradient resolves independently in this order: feed- or collection-specific remote light/dark theme, tenant/global remote light/dark theme, host-supplied light/dark theme, then its iOS default. Missing, null, incomplete, malformed, or unknown remote values continue to the next layer for that gradient only; a malformed top value does not discard a valid bottom value, and vice versa.

A clear-to-clear gradient is an explicit transparent scrim and does not fall back to the default. This can be used to remove either scrim visually while preserving its existing container and visibility behavior. Custom colours are rendered as supplied, so the integrating app is responsible for maintaining sufficient contrast between video content and Player controls or text.

Remote colours accept #RRGGBB or alpha-first #AARRGGBB values. For example:

{
  "theme": {
    "light": {
      "player": {
        "clips": {
          "topGradient": {
            "startColor": "#CC0057B8",
            "endColor": "#00000000",
            "startPosition": "topLeft",
            "endPosition": "bottomRight"
          },
          "bottomGradient": {
            "startColor": "#CCEF3340",
            "endColor": "#00000000",
            "startPosition": "bottomRight",
            "endPosition": "topLeft"
          }
        }
      }
    }
  }
}

Set theme.light.player.clips.modal and theme.dark.player.clips.modal to customize non-embedded iOS Clips Players. These options are configured by the integrating app; they do not add remote Settings keys. Leaving the object or an individual property unset preserves the existing behavior for that property. Embedded Clips keep their separate spacing and progress-position settings.

All dimensions are fixed points. Spacing, insets and gradient heights accept zero; negative or non-finite values are ignored. Progress thickness, icon size and navigation height must be positive and finite. Navigation height has a minimum of 48 points, and navigation glyphs are capped at 48 points while the Back and Search buttons retain at least 48 × 48-point targets.

Property under clips.modal Type Behavior when configured
drawBehindStatusBar Bool? Extends the content video behind the status bar while navigation remains below the safe-area top. Ad media retains its safe-area layout
feedSwitcherItemSpacing CGFloat? Gap between For You and Following
titleToCategoriesSpacing CGFloat? Gap between the title/description block and categories
categoriesToMoreSpacing CGFloat? Gap between categories and the shared more/less control
metadataToProgressBarSpacing CGFloat? Minimum gap between metadata and the resting visible progress track
progressBarToActionSpacing CGFloat? Gap between the resting progress track and primary action, in either ordering, including layouts with a bottom banner
progressBarPosition Theme.Player.Clips.ProgressBar.Position? .aboveAction places the track above a visible primary action; .bottom retains the existing bottom placement. A hidden CTA or disabled progress bar does not reserve an extra gap
topGradientHeight CGFloat? Height of the top readability gradient; unset retains 96 points
bottomGradientHeight CGFloat? Height of the bottom readability gradient, still attached to the media. Its existing visibility rule is preserved
progressBarHeight CGFloat? Resting track thickness; unset retains 2 points
progressBarActiveHeight CGFloat? Scrubbing track thickness; unset retains 8 points. The active track never becomes thinner than the resting track
progressBarInsetHorizontal CGFloat? Left and right track inset; unset retains 16 points
navigationIconSize CGFloat? Maximum dimension of Back and Search glyphs, preserving their aspect ratios
navigationInsetHorizontal CGFloat? Leading Back inset (overrides clips.spacing.backButtonStartInset) and trailing Search inset
navigationInsetTop CGFloat? Additional navigation inset below the safe-area top
navigationHeight CGFloat? Navigation row height; unset retains 48 points
searchIcon UIImage? Modal Search replacement image; unset retains the system magnifier

Spacing applies only when both rendered endpoints exist. When a bottom-positioned track sits below the CTA, metadata keeps enough clearance for the intervening button. Metadata does not move as the track expands during scrubbing. Content-specific lower spacing and progress dimensions do not alter full-screen Ad creatives.

Keep using player.icons.back for the Back image, clips.topGradient and clips.bottomGradient for gradient colors, clips.progressBar for track colors, and existing typography and action-button options. The remote theme.behavior.player.clips.modalContentBottomAnchor continues to select video- or screen-anchored lower controls independently.

var theme = StorytellerTheme()
theme.light.player.clips.modal = .init(
    drawBehindStatusBar: true,
    feedSwitcherItemSpacing: 12,
    titleToCategoriesSpacing: 8,
    categoriesToMoreSpacing: 4,
    metadataToProgressBarSpacing: 16,
    progressBarToActionSpacing: 16,
    progressBarPosition: .aboveAction,
    topGradientHeight: 211,
    bottomGradientHeight: 180,
    progressBarHeight: 2,
    progressBarActiveHeight: 8,
    progressBarInsetHorizontal: 16,
    navigationIconSize: 24,
    navigationInsetHorizontal: 0,
    navigationInsetTop: 0,
    navigationHeight: 48,
    searchIcon: UIImage(named: "clips-search")
)
theme.dark.player.clips.modal = theme.light.player.clips.modal
Storyteller.shared.theme = theme

Embedded Clips spacing#

The four optional internal spacing properties apply only to Embedded Clips. Each property resolves independently through the active feed-specific server appearance, tenant/global server appearance, and host-supplied light or dark theme. Missing, non-finite, or negative values are treated as unset and preserve the existing iOS layout for that relationship.

Configured spacing is added only when both rendered elements are present. The fixed point values do not scale with Dynamic Type, while the surrounding metadata continues to reflow. The Sky configuration uses 8, 4, 16, and 16 points for title-to-categories, categories-to-expansion, metadata-to-progress, and progress-to-action respectively.

Example:

var theme = StorytellerTheme()

theme.light.player.icons = StorytellerPlayerIcons(
    back: UIImage(named: "icon-back-custom"),
    likeInitial: UIImage(named: "custom_like_initial"),
    likeLiked: UIImage(named: "custom_like_liked"),
    likeAnimation: .init(
        liked: .init(name: "custom_like_liked_animation"),
        unliked: .init(name: "custom_like_unliked_animation")
    )
)

You can omit likeInitial and likeLiked when you want to keep the SDK's default static heart icons and only override the transition animations.

Tenant settings can also provide static Clips Player like icons through the remote theme payload. Use complete URL pairs under the light and/or dark style theme:

{
  "light": {
    "player": {
      "icons": {
        "like": {
          "initial": "https://example.com/like.png",
          "liked": "https://example.com/liked.png"
        }
      }
    }
  }
}

Both initial and liked must be provided for the SDK to replace the default static like icons. Remote settings do not configure icons.like.animation; use the local StorytellerTheme API for bundled Lottie animations.

Diagram illustrating playerTheme options


Embedded Clips video sizing#

theme.player.clips.embeddedVideoSizing supports these local values:

Value Behavior
.legacy Preserves the existing height-constrained Embedded Clips media layout
.widthConstrained Uses the complete Embedded Clips viewport width, preserves the source aspect ratio, and centres the media vertically

With .widthConstrained, landscape or other short media shows black space above and below it. Media whose width-derived height exceeds the viewport is cropped equally at the top and bottom. The video is never cropped at the left or right, and the outer viewport, controls, captions, action rail, AdChoices, and banner placement keep their existing layout.

Set the value independently on the light and dark themes:

var theme = StorytellerTheme()
theme.light.player.clips.embeddedVideoSizing = .widthConstrained
theme.dark.player.clips.embeddedVideoSizing = .widthConstrained

Leaving a local value as nil inherits theme.behavior.player.clips.embeddedVideoSizing from remote Settings. Remote Settings accepts the exact strings legacy and widthConstrained. An explicit local light or dark value always wins over the remote value. Missing, null, malformed, and unknown remote values preserve .legacy behavior without discarding other valid theme fields.

The option applies only to Embedded Clips, including supported first-party, VAST, GAM, and AdMob video Ads. Modal or fullscreen Clips, direct Player presentation, Stories, image Ads, bottom banners, and tvOS are unchanged. See Width-constrained Embedded Clips video Ads when supplying a custom view-based video Ad.


Clips progress bar position#

theme.player.clips.progressBar.position controls the progress bar hierarchy for Clips. It supports the following appearance-scoped values:

Value Behavior
.bottom / bottom Preserves the existing progress bar position. This is the default
.aboveAction / aboveAction Places the progress bar above the visible primary action in Embedded Clips

When .aboveAction is selected, modal and full-screen Clips remain unchanged. Embedded Clips without a visible primary action also keep the stable bottom position without reserving an empty action gap. The scrub gesture area follows the rendered progress bar in both positions.

The active light or dark value resolves in this order: feed-specific remote theme, tenant/global remote theme, host-supplied theme, then .bottom. Unknown or malformed remote values are ignored so normal inheritance can continue.

Example remote Settings fragment:

{
  "theme": {
    "light": {
      "player": {
        "clips": {
          "progressBar": {
            "position": "aboveAction"
          }
        }
      }
    }
  }
}

theme.behavior.player.clips.modalContentBottomAnchor is a remote Settings property for non-embedded modal Clips Players. It accepts the following values:

Value Behavior
video Keeps the title, metadata, side actions, primary action, and progress controls anchored to the bottom of the 9:16 media frame. This is the default and preserves the existing layout
screen Keeps the media top-aligned at 9:16 while anchoring the lower Player UI to the viewport safe-area bottom, using the space below the media on taller screens

Missing, null, malformed, or unknown values resolve to video. The setting does not stretch or crop video, does not affect Embedded Clips, and keeps standard Ads on the existing video-anchored layout.

Example remote Settings fragment:

{
  "theme": {
    "behavior": {
      "player": {
        "clips": {
          "modalContentBottomAnchor": "screen"
        }
      }
    }
  }
}

Followable Category Profile#

The default Followable Category profile screen is configured from remote Settings via the CMS. There is no SDK public Theme API for these profile fields.

Set theme.behavior.player.clips.enableProfileScreen to true to use the profile screen. When this value is false or missing, the SDK uses the legacy Followable Category screen. A custom category screen returned by StorytellerDelegate.viewController(for:) always takes priority over either SDK screen.

Profile appearance is configured independently under theme.light.profileScreen and theme.dark.profileScreen:

Property Default Value Data Type Description
contentAvailability.clips true Bool Shows the Latest/Popular Clips feed
contentAvailability.stories false Bool Shows the Stories row sourced from the category display title
displayTitle.textSize 22 Int Category title text size
displayTitle.lineHeight 28 Int Category title line height
displayTitle.textCase default String default, upper, or lower
displayTitle.textColor #FFFFFF String Category title color
description.textSize 16 Int Category description text size
description.lineHeight 20 Int Category description line height
description.textCase default String default, upper, or lower
description.textColor #D9FFFFFF String Category description color
followButton.cornerRadius 8 Int Follow/Unfollow button corner radius
followButton.title.textSize 14 Int Button title text size
followButton.title.lineHeight 20 Int Button title line height
followButton.title.textCase default String default, upper, or lower
followButton.followed.textColor #FFFFFF String Unfollow-state title color
followButton.followed.backgroundColor #33FFFFFF String Unfollow-state background color
followButton.unfollowed.textColor #FFFFFF String Follow-state title color
followButton.unfollowed.backgroundColor #1C62EB String Follow-state background color
tabs.title.textSize 20 Int Latest/Popular title text size
tabs.title.lineHeight 24 Int Latest/Popular title line height
tabs.title.textCase default String default, upper, or lower
tabs.selectedTextColor Light: #FF1A1A1A; Dark: #FFFFFFFF String Selected tab title color
tabs.unselectedTextColor Light: #991A1A1A; Dark: #D9FFFFFF String Unselected tab title color

Colors accept six-digit RGB (#RRGGBB) or eight-digit alpha-first ARGB (#AARRGGBB). Missing, null, malformed, or out-of-range fields fall back independently, so one invalid field does not discard valid sibling values. If both content availability values resolve to false, the SDK falls back to Clips enabled and Stories disabled.

Missing Profile tab colours follow the active appearance. Automatic UI style uses the system Light or Dark appearance, while forced Light or Dark styles use their corresponding fallback values. Explicitly configured selected or unselected colours take precedence independently.

Example remote Settings fragment:

{
  "theme": {
    "behavior": {
      "player": {
        "clips": {
          "enableProfileScreen": true
        }
      }
    },
    "light": {
      "profileScreen": {
        "contentAvailability": {
          "clips": true,
          "stories": true
        },
        "displayTitle": {
          "textColor": "#FF1A1A1A"
        },
        "tabs": {
          "selectedTextColor": "#FF1A1A1A",
          "unselectedTextColor": "#991A1A1A"
        }
      }
    }
  }
}

Text uses the resolved SDK theme font. The profile image comes from the CMS and its border is not themeable. theme.light.categoryScreen.followIcons and theme.dark.categoryScreen.followIcons continue to configure the legacy screen and are separate from profileScreen.


Cards#

The cards property applies customizations to Storyteller Cards. Cards audio behavior is controlled by tenant settings, while local theme values can replace the icons used by the Cards audio control.

Property Default Value Data Type Description
audio.mutedIcon nil UIImage? Complete 48x48 visual used for the Cards audio control when the active video Card is muted
audio.unmutedIcon nil UIImage? Complete 48x48 visual used for the Cards audio control when the active video Card is unmuted

If a Cards audio icon is not provided, the SDK uses a default 48x48 tap target with a bundled 32x32 circular state icon centered inside it. Custom theme.cards.audio images replace that whole visual directly, without an SDK-drawn circle/background; include any desired backer in the image asset. theme.player.icons.mute customizes Stories and Clips Player mute icons; use theme.cards.audio for Cards.

Example:

var theme = StorytellerTheme()

theme.light.cards.audio = .init(
    mutedIcon: UIImage(named: "icon-card-audio-muted"),
    unmutedIcon: UIImage(named: "icon-card-audio-unmuted")
)

Buttons#

The buttons property applies customizations to buttons which appear throughout the UI.

Property Default Value Data Type Description
backgroundColor inherits colors.white.primary UIColor The background color of buttons throughout the UI
textColor inherits colors.black.primary UIColor The text color of buttons throughout the SDK
textCase default StorytellerTextCasing Sets the text case for buttons throughout the UI. Possible values are upper, lower and default
cornerRadius inherits primitives.cornerRadius CGFloat The corner radius for all buttons throughout the UI

Instructions#

Use the instructions property to customize the appearance of the instructions screen.

Property Default Value Data Type Description
show true Bool Determines whether the Instructions Screen is shown the first time the Story Player or Clips Player is opened on a device. Set to false to completely disable the instructions screen.
headingColor inherits colors.black.primary for light, colors.white.primary for dark UIColor The color of the heading text on the Instructions Screen
headingTextCase default StorytellerTextCasing Determines the text case of the heading on the Instructions Screen. Possible values are upper, lower and default
headingFont null StorytellerFontProvider Defines the font of the heading text on the Instructions Screen
subHeadingColor inherits colors.black.secondary for light, colors.white.secondary for dark UIColor The color of the subheading text on the Instructions Screen
backgroundColor inherits colors.white.primary for light, colors.black.primary for dark UIColor The color of the background of the Instructions Screen
icons null StorytellerInstructionIcons A set of custom icons to be used for each instruction on the Instructions Screen
button.backgroundColor inherits colors.black.primary for light, colors.white.primary for dark UIColor The background color of the button used on the Instructions Screen
button.textColor inherits colors.white.primary for light, colors.black.primary for dark UIColor The text color of the button used on the Instructions Screen

The icons property can be used to provide a completely custom set of icons. The icons should be 48x48 PNGs. An example of using this property is shown below:

let customIcons = StorytellerInstructionIcons(
    forward: UIImage(named: "icon-forward-custom"),
    pause: UIImage(named: "icon-pause-custom"),
    back: UIImage(named: "icon-back-custom"),
    move: UIImage(named: "icon-move-custom")
)

var theme = StorytellerTheme()
theme.light.instructions.icons = customIcons

Diagram illustrating instructionsTheme options


Engagement Units#

The engagementUnits property can be used to customize properties relating to Polls and Quizzes.

Property Default Value Data Type Description
poll.answerTextColor inherits colors.black.primary UIColor The text color used for Poll Answers
poll.percentBarColor #CDD0DC UIColor The background color of the percentage bar in Poll Answers
poll.selectedAnswerBorderColor inherits colors.primary UIColor The border color applied to the selected Poll Answer
poll.answeredMessageTextColor inherits colors.white.tertiary UIColor The color of the vote count shown to users after they select a Poll Answer
poll.selectedAnswerBorderImage null UIImage? A border image which can be used for the selected Poll Answer. If this is set, selectedAnswerBorderColor is used.
poll.showImageAnswerGradientOverlay true Bool Shows the gradient overlay behind the text in image Poll Answers
poll.showPercentBarBackground false Bool Adds a striped background under the percentage bar in Poll Answers
triviaQuiz.correctColor inherits colors.success UIColor The color used to show correct answers in Trivia Quizzes
triviaQuiz.incorrectColor inherits colors.alert UIColor The color used to show incorrect answers in Trivia Quizzes

Diagram illustrating pollTheme options

Diagram illustrating quizTheme options


Sheets#

The sheets property controls the top corners of Storyteller content Sheets and the Search Filters sheet. See Configure Sheet corner radius for remote configuration, precedence, validation, defaults, and iOS availability.

Property Default Value Data Type Description
sheets.cornerRadius nil CGFloat? Optional top-corner radius in points. nil preserves each Sheet surface's compatibility default.

Clip lists in Search retain their loaded results when their Player closes, regardless of the remote theme.behavior.reloading.reloadOnExit setting. This applies whether or not the viewer opened a category and preserves the Search journey. Other lists keep their configured reloading behavior. See Clip Category Navigation.

The search property applies customizations to suggestions, no-results, results and filters in the Search component. Every new Search appearance property is optional. Omitted values keep the existing iOS appearance, and theme.light and theme.dark resolve independently.

Search text uses customFont with the semantic weight for each element. search.heading.font remains the font override for the Filters title, while Search result section headings continue to use lists.title. Configure the Apply Filters button through search.filters.applyButton; each remote or host-app field resolves independently before the compatibility default. Shared buttons values do not affect this Search action.

Property Default Value Data Type Description
search.backgroundColor colors.white.primary in light mode; colors.black.primary in dark mode UIColor? Background color across Search states
search.backIcon chevron.backward system image UIImage? Image to be used as a back icon in the Search UI
search.heading.font inherits customFont StorytellerFontProvider? Font override for the Filters title
search.heading.textSize 22 CGFloat Size of the Filters title
search.heading.lineHeight 28 CGFloat Line height of the Filters title
search.heading.textCase default StorytellerTextCasing Text case for the Filters title
search.heading.textColor inherits lists.title.textColor UIColor Color of Filter View title
search.input.backgroundColor existing Search field fill UIColor? Search field background color
search.input.textColor current light/dark primary text color UIColor? Entered search text color
search.input.placeholderTextColor system placeholder color UIColor? Search placeholder color
search.input.iconColor system gray UIColor? Search and clear icon color
search.input.cornerRadius existing Search field radius CGFloat? Search field corner radius
search.input.textSize 16 CGFloat? Search field text size
search.input.lineHeight font default CGFloat? Optional Search field line height
search.filterButton.backgroundColor clear UIColor? Filter button background color
search.filterButton.iconColor current light/dark primary text color UIColor? Filter icon color
search.filterButton.cornerRadius 0 CGFloat? Filter button corner radius
search.suggestions.textColor current light/dark primary text color UIColor? Suggestion text color
search.suggestions.iconColor current light/dark primary text color UIColor? Suggestion icons color
search.suggestions.iconBackgroundColor existing Search field fill UIColor? Magnifying-glass icon background color
search.suggestions.textSize 16 CGFloat? Suggestion text size
search.suggestions.lineHeight font default CGFloat? Optional suggestion line height
search.noResults.iconColor #B0B0B4 UIColor? No-results icon color
search.noResults.title.textColor current light/dark primary text color UIColor? No-results title color
search.noResults.title.textSize inherits lists.title.textSize CGFloat? No-results title size
search.noResults.title.lineHeight inherits lists.title.lineHeight CGFloat? No-results title line height
search.noResults.title.textCase default StorytellerTextCasing? No-results title text case
search.noResults.message.textColor current light/dark tertiary text color UIColor? No-results message color
search.noResults.message.textSize 16 CGFloat? No-results message size
search.noResults.message.lineHeight font default CGFloat? Optional no-results message line height
search.noResults.message.textCase default StorytellerTextCasing? No-results message text case
search.filters.backgroundColor current light/dark background color UIColor? Filter sheet background color
search.filters.handleColor system secondary color UIColor? Filter sheet drag-handle color
search.filters.sectionHeading.textColor current light/dark primary text color UIColor? Filter section heading color
search.filters.sectionHeading.textSize 16 CGFloat? Filter section heading size
search.filters.sectionHeading.lineHeight font default CGFloat? Optional filter section heading line height
search.filters.sectionHeading.textCase default StorytellerTextCasing? Filter section heading text case
search.filters.option.backgroundColor existing Search field fill UIColor? Unselected filter option background color
search.filters.option.textColor current light/dark primary text color UIColor? Unselected filter option text color
search.filters.option.borderColor clear UIColor? Unselected filter option border color
search.filters.option.selectedBackgroundColor existing Search field fill UIColor? Selected filter option background color
search.filters.option.selectedTextColor current light/dark primary text color UIColor? Selected filter option text color
search.filters.option.selectedBorderColor current light/dark primary text color UIColor? Selected filter option border color
search.filters.option.cornerRadius inherits primitives.cornerRadius CGFloat? Filter option corner radius
search.filters.option.textSize 16 CGFloat? Filter option text size
search.filters.option.lineHeight font default CGFloat? Optional filter option line height
search.filters.applyButton.backgroundColor black in light mode; white in dark mode UIColor? Apply Filters button background color
search.filters.applyButton.textColor white in light mode; black in dark mode UIColor? Apply Filters button text color
search.filters.applyButton.textCase default StorytellerTextCasing Apply Filters button text case
search.filters.applyButton.cornerRadius inherits primitives.cornerRadius CGFloat? Apply Filters button corner radius
var theme = StorytellerTheme()

theme.light.search.backgroundColor = UIColor.white
theme.light.search.input.backgroundColor = UIColor.systemGray6
theme.light.search.input.iconColor = UIColor.systemBlue
theme.light.search.filterButton.backgroundColor = UIColor.systemBlue
theme.light.search.filterButton.iconColor = UIColor.white
theme.light.search.noResults.title.textColor = UIColor.systemBlue
theme.light.search.filters.option.selectedBorderColor = UIColor.systemBlue
theme.light.search.filters.applyButton.backgroundColor = UIColor.systemBlue
theme.light.search.filters.applyButton.textColor = UIColor.white

theme.dark.search.backgroundColor = UIColor.black
theme.dark.search.input.backgroundColor = UIColor.secondarySystemBackground
theme.dark.search.filters.applyButton.backgroundColor = UIColor.white
theme.dark.search.filters.applyButton.textColor = UIColor.black

Storyteller.shared.theme = theme

Example#

Start with Creating Themes, then use the property reference above to add only the overrides your integration needs. For a complete custom-font setup, including app registration and font-name checks, see Configure custom fonts.


Home#

The home property can be used to customize properties related to the Storyteller Home component.

Property Default Value Data Type Description
home.headerTitle.font nil StorytellerFontProvider The only font that can vary from theme.font, defines the font for the heading
home.headerTitle.textSize 22 CGFloat Size of the title in section
home.headerTitle.lineHeight 28 CGFloat The line height of the title on in section
home.headerTitle.textCase default StorytellerTextCasing Sets the text case for buttons throughout the UI. Possible values are upper, lower and default
home.headerTitle.textColor nil UIColor Color of heading text in Storyteller Home
home.circularTitle.textSize 11 CGFloat Size of the circular title in section
home.circularTitle.lineHeight 13 CGFloat The line height of the circular title on in section
home.singletonTitle.textSize 22 CGFloat Size of the singleton title in section
home.singletonTitle.lineHeight 28 CGFloat The line height of the singleton title in section
home.gridTitle.textSize 16 CGFloat Size of the grid title in section
home.gridTitle.lineHeight 22 CGFloat The line height of the grid title on in section