AIOStreamsAIOStreams
Reference

Web app themes and custom CSS

Change the web app's colours, restyle it with your own CSS, and the selectors to style its parts by.

The web app's Theme settings, under Settings → Theme, change its look for you on every device you sign in on. Each user of a server has their own.

  • Presets set an accent and a background colour together.
  • Accent colours buttons, progress bars and highlights.
  • Background colours pages and panels, which take their shades from it. Dark colours read best.
  • Custom CSS goes on top of the theme, for anything the colours don't cover.

Adding custom CSS

Write your CSS in the Custom CSS box and press Apply CSS. A message then asks whether to keep it: press Keep, or it is undone after 15 seconds. That way CSS that hides the page, or the button, can't lock you out. Your text stays in the box either way, so you can fix it and try again.

Once kept, it is saved to your account and applies on every device you sign in on. It doesn't apply on the sign-in screens, since nobody is signed in there yet.

If your CSS ever breaks the app on some device, add ?safe to the address (for example https://example.com/web/?safe). Custom CSS stays off for that visit, so you can fix or clear it in the settings.

Finding what to style

Parts of the app carry a data-ui attribute naming what they are, and other data-* attributes saying which one or what state it's in. Aim your CSS at these rather than at class names, which change between versions.

To find a part's name, right-click it in a browser and choose Inspect, then look at the data-ui on it or on the elements around it. The reference below lists them all.

/* Every progress bar */
[data-ui='progress-bar-fill'] {
  background: hotpink;
}

/* Only those in the continue watching row */
[data-row='resume'] [data-ui='progress-bar-fill'] {
  background: hotpink;
}

Examples

True black

The Background setting does this best, since panels take their shades from it. With CSS:

:root {
  --background: #000;
  --paper: #000;
  --color-gray-950: 0 0 0;
  --color-gray-900: 10 10 10;
}

Squarer corners

[data-ui='poster-card-image'],
[data-ui='wide-card-image'],
[data-ui='episode-image'] {
  border-radius: 0.25rem;
}

Dim what you've watched

:is([data-ui='poster-card'], [data-ui='wide-card'])[data-watched]
  [data-ui='artwork'] {
  opacity: 0.4;
}

No zoom on hover

[data-ui='poster-card']:is(:hover, :focus-visible) [data-ui='artwork'],
[data-ui='wide-card']:hover [data-ui='artwork'],
[data-ui='wide-card'] :focus-visible [data-ui='artwork'] {
  transform: none;
}

Bigger posters in rows

[data-ui='media-row'][data-shape='poster'] [data-ui='media-row-item'] {
  flex-basis: 14rem;
}

Hide a row on the home page

[data-row='upcoming'] {
  display: none;
}

A catalog's row is named view: and then its id. To find the id, open the catalog by clicking the row's title: the id is the part of the address after /discover/.

[data-row='view:abc123'] {
  display: none;
}

To hide the featured banner, set Featured on home to Nothing under Settings → Interface instead.

Hide what a catalog holds

A catalog's row on the home page says what it holds, such as Shows or Movies, beside its title.

[data-ui='media-row'][data-row^='view:'] [data-ui='media-row-title'] a > span {
  display: none;
}

Hide spoilers

Blur the pictures, titles and descriptions of episodes you haven't watched. Pointing at one fades it in.

/* Episodes you haven't watched, until you point at one */
:is([data-ui='episode-card'], [data-ui='episode-list-item']):not(
    [data-watched],
    :hover,
    :focus-within
  )
  [data-ui='artwork'] {
  filter: blur(12px);
}
:is([data-ui='episode-card'], [data-ui='episode-list-item']):not(
    [data-watched],
    :hover,
    :focus-within
  )
  :is(
    [data-ui='episode-title'] > span,
    p[data-ui='episode-title'],
    [data-ui='episode-synopsis']
  ) {
  filter: blur(5px);
}

/* Next up and upcoming episodes, but not what you're partway through */
:is([data-row='next-up'], [data-row='upcoming'], [data-row='resume'])
  [data-ui='wide-card']:not([data-in-progress], :hover, :focus-within)
  [data-ui='artwork'] {
  filter: blur(12px);
}
:is([data-row='next-up'], [data-row='upcoming'], [data-row='resume'])
  [data-ui='wide-card']:not([data-in-progress], :hover, :focus-within)
  [data-ui='wide-card-subtitle'] {
  filter: blur(5px);
}

/* Fade the blur in and out */
[data-ui='artwork'] {
  transition-property: filter, transform, opacity;
}
[data-ui='episode-title'] > span,
p[data-ui='episode-title'],
[data-ui='episode-synopsis'],
[data-ui='wide-card-subtitle'] {
  transition: filter 0.3s;
}

Blur the text inside an episode's title rather than the title itself: the title is what makes the whole episode play when pressed, and a blur on it would shrink that to the title alone.

Hide buttons on title pages

[data-ui='item-action']:is([data-name='trailer'], [data-name='drop']) {
  display: none;
}

Hide the score, and the dot before it

[data-ui='item-meta'] [data-name='score'],
[data-ui='meta-separator']:has(+ [data-name='score']) {
  display: none;
}

Square cast photos

[data-ui='person-card-image'] {
  border-radius: 0.75rem;
}

Versions

/* Mark cached versions */
[data-ui='version'][data-cached] {
  border-color: rgb(var(--color-brand-500) / 0.6);
  background: rgb(var(--color-brand-500) / 0.08);
}

/* Ring the top version, since the list is sorted best first */
[data-ui='version']:nth-child(1 of [data-ui='version']) {
  box-shadow: 0 0 0 1px rgb(var(--color-brand-400));
}

/* Names in a fixed-width font, for formats laid out in columns */
[data-ui='version-name'],
[data-ui='version-description'] {
  font-family: ui-monospace, monospace;
}

/* A wider list on big screens */
[data-ui='dialog'][data-name='versions'] {
  max-width: 72rem;
}

Leave choosing which versions appear to your filters, rather than hiding some with CSS. Hidden versions still count, and playing straight away or carrying on to the next episode can still pick them.

The player

/* A red timeline, thicker on hover */
[data-ui='seek-bar-progress'] {
  background: #e50914;
}
[data-ui='seek-bar-track'] {
  height: 6px;
}
[data-ui='seek-bar']:hover [data-ui='seek-bar-track'] {
  height: 10px;
}

/* Drop buttons you never use */
[data-ui='player-button']:is([data-name='speed'], [data-name='fit']) {
  display: none;
}

/* Bigger track lists, with the chosen one in the accent colour */
[data-ui='player-menu-item'] {
  font-size: 1.1rem;
}
[data-ui='player-menu-item'][data-selected] {
  color: rgb(var(--color-brand-300));
}

To change how subtitles look in a browser, style video::cue. The desktop and Android apps draw subtitles themselves, so use Settings → Subtitles there.

video::cue {
  font-family: 'Trebuchet MS', sans-serif;
}

Your own font

@import has to come first in the box.

@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;700&display=swap');

body {
  font-family: 'Outfit', sans-serif;
}
[data-ui='brand-logo'] {
  content: url('https://example.com/logo.png');
}

Hide the scrollbar

html {
  scrollbar-width: none;
}
::-webkit-scrollbar {
  display: none;
}

Messages in the bottom right

On a phone they stay at the top, where they span the screen.

@media (min-width: 640px) {
  [data-sonner-toaster] {
    top: auto !important;
    bottom: 24px !important;
    left: auto !important;
    right: 24px !important;
    transform: none !important;
  }

  /* Each message sits at the bottom, with older ones stacked above */
  [data-sonner-toast] {
    top: auto !important;
    bottom: 0 !important;
    --lift: -1 !important;
    --lift-amount: calc(-1 * var(--gap)) !important;
  }
}

No fade-ins

[data-page],
[data-ui='media-row-item'] > div,
[data-ui='card-grid'] > div {
  opacity: 1 !important;
  transform: none !important;
  top: 0 !important;
}

Only in one app

/* A bigger timeline in the desktop app only */
html[data-host='desktop'] [data-ui='seek-bar-track'] {
  height: 8px;
}

When a rule doesn't take

Your CSS comes after the app's own, so a rule as specific as the app's wins. When one doesn't:

  • Hover and focus styles need the same state in your selector. The zoom on a hovered poster, for example, is only undone by [data-ui='poster-card']:hover [data-ui='artwork'].
  • Animations set their styles on the element itself, which only !important beats. Fade-ins and the message popups are like this.
  • Class names change between versions, so a rule aimed at one can stop working after an update. The data-ui names and the shared component classes are kept stable.

What CSS can't change

  • Subtitles in the desktop and Android apps, which the player draws. Use Settings → Subtitles.
  • The page while a video plays in the desktop app. It has to stay see-through for the video beneath it, so custom CSS can't give it a background then.
  • The sign-in screens, before anyone is signed in.

Selectors

States are attributes set only while true, such as data-watched. Match them with [data-watched], or :not([data-watched]) for the opposite.

The whole page

SelectorWhat it is
html[data-host]Which app you're in: browser, desktop (AIOStreams' desktop app), jellyfin-desktop, android or jellyfin-app (another Jellyfin app that loads the web client)
html[data-platform]In the desktop app, the system: windows, linux or macos
html.playingSet while a video plays
[data-page]Each page's container: home, discover, library (a catalog), search, favourites, calendar, history, settings, item, person, player or sign-in
[data-page='sign-in']The screens before you're signed in; data-name is sign-in, quick-connect, user-picker, servers or unreachable
page-backgroundThe colour behind every page
page-body, page-header, page-titleA page's padded column, its heading row and its title
section, section-titleA titled part of a page; data-name is episodes, collection, details or filmography
sidebarThe sidebar. Its items are .UI-VerticalMenu__item, with data-name (home, discover, search, favourites, calendar, activity, settings, account) and data-current="true" on the page you're on
sidebar-logo, brand-logoThe logo in the sidebar, and the logo itself (also on the sign-in screens)
sidebar-historyThe back and forward buttons in the sidebar, where the browser has none; data-name is back or forward
mobile-nav, mobile-nav-itemThe bar at the bottom on a phone; items have data-name as in the sidebar, and aria-current on the page you're on
account-menuThe menu under your picture; its items have data-name: activity, calendar, settings, switch-user, configure, change-server, sign-out
window-controlsThe window buttons in the desktop app; each has data-name: minimise, maximise, fullscreen or close
pill-tabs, pill-tabA row of choices such as Movies / Shows; data-name says which row, data-selected marks the choice
dialogA dialog box; data-name is versions, overview, all-episodes, calendar-day, sync-to-line or external-player
avatarA user's picture or initial

Rows, grids and cards

SelectorWhat it is
home-rowsEverything under the featured banner on the home page
media-rowA scrolling row. data-row names it: resume, next-up, upcoming, now-playing, similar, cast, crew, seasons, episodes:<season id>, sub-collections, search:movies, search:shows, or view:<id> for a catalog. data-shape is poster, wide or square
media-row-title, media-row-action, media-row-nav, media-row-itemA row's title, the button beside it (such as Activity), its arrows, and each card's slot
card-gridA grid of cards; data-shape is poster or wide, data-size the poster size setting (small, medium, large)
poster-card, poster-card-image, poster-card-shade, poster-card-text, poster-card-title, poster-card-subtitleA poster, its picture, the shade over it on hover, and the lines under it. data-shape is poster, landscape or square. States: data-watched, data-in-progress
wide-card, wide-card-image, wide-card-play, wide-card-badge, wide-card-text, wide-card-title, wide-card-subtitle, wide-card-metaA landscape card, as in continue watching, with its play icon and corner badge. States: data-watched, data-in-progress, data-highlighted, data-dimmed, data-unavailable
artwork, artwork-fallbackA card's picture, and the title shown when it has none
progress-bar, progress-bar-fillHow far through something you are
watched-badge, unwatched-countThe tick on something watched, and the count of episodes left on a show
focus-ring, opened-pulseThe ring on a card the keyboard, a remote or a gamepad is on, and the brief one on the episode a page opened on
item-menuThe menu a card opens on right-click or a long press; its items have data-name: play, play-now, choose-version, open, watched, watched-up-to, remove-resume, favourite
SelectorWhat it is
heroThe banner; data-follow when it follows the selected card, data-loading while it loads
hero-backdrop, backdrop-frameThe picture, and the box holding it
hero-shadeThe shading over the picture; data-name is side or bottom
hero-content, hero-logo, hero-title, hero-overviewThe text part, the logo or title, and the description
hero-metaThe line of facts; each has data-name (year or episode, score, genres), with a meta-separator between
hero-actions, hero-actionThe buttons; data-name is play or info
hero-dots, hero-dotThe dots for each title; data-current marks the one showing
hero-rowsThe rows scrolling under the banner when it follows the selected card

Title pages

SelectorWhat it is
item-pageThe page; data-type is Movie, Series or BoxSet. States: data-watched, data-favourite
item-backdrop, item-backdrop-image, item-backdrop-shadeThe backdrop, its picture (data-wash when a blurred poster stands in) and its shading (data-name is side or bottom)
item-body, item-headerEverything over the backdrop, and the top part
item-poster, item-logo, item-title, item-tagline, item-next-airing, item-genres, item-genre, item-detailsThe parts of the header and the details list
item-metaThe line of facts; each has data-name (year, age-rating, runtime, score, status), with a meta-separator between
item-actions, item-actionThe buttons; data-name is play, restart, trailer, watched, favourite or drop, and data-active marks watched, favourited or dropped
item-actions-dividerThe line before the links to other sites
overview, overview-more, overview-popover, overview-banner, overview-textA description, its More link and the full text it opens
external-linksThe links to other sites
season-poster, season-pillA season to pick; data-selected marks the open one
season-summary, season-overviewThe season's year and episode count, and its description
episode-card, episode-list-item, episode-listAn episode in a row or a list. States: data-watched, data-in-progress, data-highlighted (the one you came to), and data-unavailable (unaired or missing)
episode-image, episode-play, episode-number, episode-status, episode-head, episode-kicker, episode-title, episode-synopsisThe parts of an episode
episode-actions, episode-actionAn episode's buttons; data-name is details or watched, and data-active marks a watched one
person-card, person-card-image, person-card-name, person-card-roleSomeone in the cast or crew
person-header, person-photo, person-datesA person's page

Versions

SelectorWhat it is
dialog[data-name='versions']The version list's dialog box
versions-banner, versions-header, versions-count, versions-listThe parts of the list
versions-actionThe buttons at the top; data-name is details or search-again
versions-start, versions-filterThe choice between resuming and starting over, and the box to filter versions
versionOne version; data-playing marks the one playing, data-cached a cached one
version-play, version-play-iconThe part you press to play it, and its play icon
version-name, version-description, version-playingIts title, the lines under it, and the Playing label
version-action, version-menuIts buttons, and the menu that holds them on a phone; data-name is copy-link
version-noticeA message instead of a version; data-type says what kind

Player

SelectorWhat it is
videoThe video, in a browser
player-controlsEverything over the video. States: data-visible while the controls show, data-paused, data-waiting while it loads
player-top-bar, player-title, player-bottom-bar, player-middle, player-timeThe bars and the middle buttons
player-buttonA button in the bars; data-name is exit, play, back, forward, previous, next, mute, subtitles, chapters, versions, audio, speed, fit, statistics or fullscreen
player-middle-buttonA button in the middle, on phones and touch screens; data-name as above
player-menu, player-menu-itemThe list a button opens; data-name says which (subtitles, chapters, audio, speed, statistics), and data-selected marks the chosen item
player-menu-actionAn item after a list's choices; data-name is add-file
volumeThe volume button and slider
volume-bar, volume-track, volume-level, volume-thumb, volume-valueThe slider, its filled part, its handle and the level as a number
volume-boost, volume-boost-levelPast 100%, where the desktop app can make it louder, and the red over the filled part that deepens the louder it gets
seek-bar, seek-bar-track, seek-bar-buffered, seek-bar-progress, seek-bar-thumb, seek-bar-tooltipThe timeline
seek-bar-segment, skip-segmentAn intro, recap or credits mark, and the button to skip it; data-type is Intro, Recap, Outro, Preview or Commercial
seek-bar-chapterA tick where a chapter starts, in the desktop app
play-flash, seek-flash, player-noticeWhat pops up on play, pause, skipping and subtitle changes
next-episode-card, next-episode-image, next-episode-action, next-episode-countdownThe next episode's card near the end; its buttons have data-name play or hide
sync-by-ear, sync-lines, sync-lineSubtitle syncing; data-closest marks the line nearest now
player-cover, player-cover-image, player-errorWhat shows while a video loads, and when it fails
subtitle-dropWhat shows while a file is dragged over the player
player-externalIn the desktop app, the note that the video plays in your own player's window

Other pages

SelectorWhat it is
search-input, search-results, search-history, search-history-itemThe search page
discover-filters, genre-pills, genre-pillDiscover's filters
calendar-grid, calendar-agenda, calendar-day, calendar-entryThe calendar as a month or a list; data-today marks today, data-watched a watched episode
history-day, history-tiles, history-tile, history-entry, history-entry-menu, history-table, history-view, user-chipsThe activity page; data-open marks an expanded tile
settings-header, settings-card, settings-card-title, settings-card-body, theme-preset, custom-css-editorThe settings page. Each tab carries data-name, and each card a data-name from its title, such as next-episode
auth-card, user-picker, user-picker-user, serverSigning in, choosing a user and choosing a server
shortcuts-help, shortcut-keys, key-capsThe shortcuts list ? opens, a shortcut's keys in settings, and the keys shown for one

Shared components

Menus, dialogs, buttons and form controls come from a shared set of components. Their parts carry a class named UI-<Component>__<part>, kept stable like the data-ui names:

ClassWhat it is
.UI-Modal__content, __overlay, __header, __title, __description, __closeA dialog box and its parts
.UI-DropdownMenu__root, __item, __label, __separatorA menu
.UI-ContextMenu__root, __item, __label, __separatorA right-click menu
.UI-Tooltip__root, .UI-Popover__root, .UI-HoverCard__rootTooltips and popups
.UI-Button__root, .UI-IconButton__rootButtons
.UI-Badge__rootA small label, such as a genre
.UI-Input__root, .UI-Textarea__root, .UI-Select__root, .UI-Switch__rootForm controls
.UI-Card__rootA panel
.UI-VerticalMenu__itemA sidebar item
.UI-Carousel__itemA card's slot in a row
.UI-Skeleton__rootA placeholder while something loads

The popup messages use [data-sonner-toaster] for their container and [data-sonner-toast] for each one, with data-type set to success, error, warning or info.

Colours

The theme's colours are CSS variables, which you can set in your CSS as well. Those holding an R G B triple are used as rgb(var(--color-brand-500)), or rgb(var(--color-brand-500) / 0.5) to make them see-through.

VariableWhat it is
--color-brand-50 to --color-brand-950The accent's shades, as R G B
--color-gray-50 to --color-gray-950The background's shades, used by panels and text, as R G B
--backgroundThe page's colour
--paperThe colour of menus and dialogs
--foreground, --muted, --muted-highlightText, dimmed text, and dimmed text on hover
--border, --ringBorders, and the ring around what has keyboard focus
--subtle, --subtle-highlightFaint fills, such as behind a hovered item
--brandAccent-coloured text
--radius, --radius-mdCorner rounding of menus and inputs
--red, --orange, --green, --blue and the likeNamed colours for text

On this page