Skip to content

Customising Material for MkDocs

Backdrop

First let's talk about the mkdocs-material you get then you sudo pacman -S mkdocs-material. Examine the output of pacman -Ql mkdocs-material (only showing 10 of the 10,000+ lines):

mkdocs-material /usr/
mkdocs-material /usr/lib/
mkdocs-material /usr/lib/python3.13/
mkdocs-material /usr/lib/python3.13/site-packages/
mkdocs-material /usr/lib/python3.13/site-packages/material/
mkdocs-material /usr/lib/python3.13/site-packages/material/__init__.py
mkdocs-material /usr/lib/python3.13/site-packages/material/__pycache__/
mkdocs-material /usr/lib/python3.13/site-packages/material/__pycache__/__init__.cpython-313.opt-1.pyc
mkdocs-material /usr/lib/python3.13/site-packages/material/__pycache__/__init__.cpython-313.pyc
mkdocs-material /usr/lib/python3.13/site-packages/material/extensions/
...

Almost all of the installed files are under /usr/lib/python3.13/site-packages/material/. This directory is the material theme.

>>> import material
>>> print(material.__file__)
/usr/lib/python3.13/site-packages/material/__init__.py

This material is the result of compilation process that takes the scss and js files in the src/templates directory of the mkdocs-material project: https://github.com/squidfunk/mkdocs-material/tree/master/src/templates.

It is this src/templates folder you need to work with to really customize the material for mkdocs theme, but the arch mkdocs-material doesn't have it (which is the way it should be).

Hopefully this gives you some context behind why we are are cloning the mkdocs-material project, using pip/venv rather than the pacman/arch.

First setup

reference: https://squidfunk.github.io/mkdocs-material/customization/#environment-setup-material-for-mkdocs

Every new mkdocs project (which will ofcourse be a website) in our workflow will involve two directories:

vector@ArchLinuxVS ~ $ tree -L 1 /home/vector/development/mkdocs
/home/vector/development/mkdocs
├── mkdocs-material
└── site

...where mkdocs-material is the result of git clone --depth 1 https://github.com/squidfunk/mkdocs-material.

$ cd mkdocs-material
$ python -m venv venv
$ source venv/bin/activate
$ pip install -e ".[recommended]"
$ hash -r
$ pip install nodeenv
$ hash -r
$ nodeenv -p -n lts
$ npm install

Now take a step back, into mkdocs folder and create the site with mkdocs command, but first make sure $ which mkdocs command points to the binary in venv (this was installed with pip install -e ".[recommended]" earlier).

$ cd ..
$ which mkdocs
/home/vector/development/mkdocs/mkdocs-material/venv/bin/mkdocs
$ mkdocs new site

mkdocs vs mkdocs-material

mkdocs-material is a theme for mkdocs. After installing mkdocs-material you can import material in python.

>>> import material
>>> print(material.__file__)
/home/vector/development/mkdocs/mkdocs-material/material/__init__.py

Now I edited mkdocs/mkdocs-material/tools/build/_/index.ts so that any modication made to templates in src directory will lead to a recompiling and output produced in mkdocs/mkdocs-material/material/templates.

mkdocs/mkdocs-material/tools/build/_/index.ts
import * as chokidar from "chokidar"
import * as fs from "fs/promises"
import * as path from "path"
import {
  EMPTY,
  Observable,
  filter,
  from,
  fromEvent,
  identity,
  catchError,
  defer,
  map,
  mergeWith,
  of,
  switchMap,
  tap
} from "rxjs"
import glob from "tiny-glob"

/* ----------------------------------------------------------------------------
 * Helper types
 * ------------------------------------------------------------------------- */

/**
 * Resolve options
 */
interface ResolveOptions {
  cwd: string                          /* Working directory */
  watch?: boolean                      /* Watch mode */
  dot?: boolean                        /* Hidden files or directories */
}

/**
 * Watch options
 */
interface WatchOptions {
  cwd: string                          /* Working directory */
}

/* ----------------------------------------------------------------------------
 * Data
 * ------------------------------------------------------------------------- */

/**
 * Base directory for compiled files
 */
export const base = "material"

/**
 * Cache to omit redundant writes
 */
export const cache = new Map<string, string>()

/* ----------------------------------------------------------------------------
 * Helper Ffunctions
 * ------------------------------------------------------------------------- */

/**
 * Return the current time
 *
 * @returns Time
 */
function now() {
  const date = new Date()
  return [
    `${date.getHours()}`.padStart(2, "0"),
    `${date.getMinutes()}`.padStart(2, "0"),
    `${date.getSeconds()}`.padStart(2, "0")
  ]
    .join(":")
}

/* ----------------------------------------------------------------------------
 * Functions
 * ------------------------------------------------------------------------- */

/**
 * Resolve a pattern
 *
 * @param pattern - Pattern
 * @param options - Options
 *
 * @returns File observable
 */
export function resolve(
  pattern: string, options?: ResolveOptions
): Observable<string> {
  return from(glob(pattern, { dot: true, ...options }))
    .pipe(
      catchError(() => EMPTY),
      switchMap(files => from(files).pipe(

        /* Start file watcher */
        options?.watch
          ? mergeWith(watch(files, options))
          : identity
      )),

      /* Build overrides */
      !process.argv.includes("--all")
        ? filter(file => !file.startsWith(`.overrides${path.sep}`))
        : identity,
    )
}

/**
 * Watch all given files
 *
 * @param files - Files
 * @param options - Options
 *
 * @returns File observable
 */
// export function watch(
//   files: string[], options: WatchOptions
// ): Observable<string> {
//   return fromEvent(
//     chokidar.watch(files, options),
//     "change", file => file // see https://t.ly/dli_k
//   ) as Observable<string>
// }

export function watch(
  patterns: string[], options: WatchOptions
): Observable<string> {
  // If the patterns look like file paths (no wildcards),
  // watch the containing directories instead
  const watchTargets = patterns.map(p => {
    if (!p.includes('*') && !p.includes('?')) {
      // Get the directory of the file
      return path.dirname(p);
    }
    return p;
  });

  return fromEvent(
    chokidar.watch(watchTargets, {
      ...options,
      ignoreInitial: true  // Don't emit events for initial scan
    }),
    "all", // Watch all events, not just "change"
    (eventName, file) => file // Return the file path
  ).pipe(
    filter(file => patterns.some(pattern => {
      // If the pattern is a direct file path, match exactly
      if (!pattern.includes('*') && !pattern.includes('?')) {
        return file === pattern;
      }
      // Otherwise use a simple glob matcher
      return file.endsWith(path.extname(pattern));
    }))
  ) as Observable<string>;
}

/* ------------------------------------------------------------------------- */

/**
 * Recursively create the given directory
 *
 * @param directory - Directory
 *
 * @returns Directory observable
 */
export function mkdir(directory: string): Observable<string> {
  return defer(() => fs.mkdir(directory, { recursive: true }))
    .pipe(
      map(() => directory)
    )
}

/**
 * Read a file
 *
 * @param file - File
 *
 * @returns File data observable
 */
export function read(file: string): Observable<string> {
  return defer(() => fs.readFile(file, "utf8"))
}


/**
 * Write a file, but only if the contents changed
 *
 * @param file - File
 * @param data - File data
 *
 * @returns File observable
 */
export function write(file: string, data: string): Observable<string> {
  let contents = cache.get(file)
  if (contents === data) {
    return of(file)
  } else {
    cache.set(file, data)
    return defer(() => fs.writeFile(file, data))
      .pipe(
        map(() => file),
        process.argv.includes("--verbose")
          ? tap(file => console.log(`${now()} + ${file}`))
          : identity
      )
  }
}

Now do:

npm start

The template wills will be generated in mkdocs-material/material/templates folder.

templates $ ls
404.html  base.html  blog-post.html  __init__.py  mkdocs_theme.yml  __pycache__
assets    blog.html  fragments       main.html    partials          redirect.html

Take note of the overrides directory in src.

mkdocs-material/src/overrides

The contents of this directory shows a sample site implementation using mkdocs material.

vector@ArchLinuxVS ~/development/mkdocs/mkdocs-material/src $ tree overrides
overrides
├── assets
│   ├── javascripts
│   │   ├── components
│   │   │   ├── _
│   │   │   │   └── index.ts
│   │   │   ├── iconsearch
│   │   │   │   ├── _
│   │   │   │   │   └── index.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── query
│   │   │   │   │   └── index.ts
│   │   │   │   └── result
│   │   │   │       └── index.ts
│   │   │   ├── index.ts
│   │   │   └── sponsorship
│   │   │       └── index.ts
│   │   ├── custom.ts
│   │   ├── integrations
│   │   │   ├── analytics
│   │   │   │   └── index.ts
│   │   │   └── index.ts
│   │   └── templates
│   │       ├── iconsearch
│   │       │   └── index.tsx
│   │       ├── index.ts
│   │       └── sponsorship
│   │           └── index.tsx
│   └── stylesheets
│       ├── custom
│       │   ├── layout
│       │   │   ├── _banner.scss
│       │   │   ├── _hero.scss
│       │   │   ├── _iconsearch.scss
│       │   │   └── _sponsorship.scss
│       │   └── _typeset.scss
│       └── custom.scss
├── home.html
├── hooks
│   ├── shortcodes.py
│   ├── translations.html
│   └── translations.py
└── main.html

Now create a symbolic link to the mkdocs/material/templates in the new site you are developing

cd site
ln -s /home/vector/development/mkdocs/mkdocs-material/material/templates /home/vector/development/mkdocs/sreema/overrides
mkdocs.yml
site_name: My Docs
theme:
  name: material
  custom_dir: overrides
mkdocs serve --watch-theme -a 127.0.0.1:8005

Its probably not best practice to use the templates folder like this. But I'm at the point where I value control and I wannt see for myself if I can make really customized sites with mkdocs.

Now let's develop the site.

systemd automation

Update: 2025-06-18

During development I think its better to run these commands from the shell rather than automate with systemd. This way you'll get real time updates during development.

To make your life easier, consider making these systemd files to autostart npm run and mkdocs serve commands.

~/.config/systemd/user/sreema-npm.service
[Unit]
Description=Start the mkdocs server

[Service]
Environment=PATH=/home/vector/devel/sreema_filaments/mkdocs-material/venv/bin:/usr/local/bin:/usr/bin:/bin
WorkingDirectory=/home/vector/devel/sreema_filaments/mkdocs-material
ExecStart=/home/vector/devel/sreema_filaments/mkdocs-material/venv/bin/npm start

[Install]
WantedBy=default.target
~/.config/systemd/user/sreema-site.service
[Unit]
Description=Start the mkdocs server

[Service]
Environment=PATH=/home/vector/devel/sreema_filaments/mkdocs-material/venv/bin:/usr/local/bin:/usr/bin:/bin
WorkingDirectory=/home/vector/devel/sreema_filaments/site
ExecStart=/home/vector/devel/sreema_filaments/mkdocs-material/venv/bin/mkdocs serve -a 127.0.0.1:7000 --watch-theme

[Install]
WantedBy=default.target

Confirm each works and enable them:

systemctl --user start sreema-npm
systemctl --user enable sreema-npm
systemctl --user start sreema-site
systemctl --user enable sreema-site

Modifying logo appearance

Since I was dealing with an non-typical logo (3 png format logos laid side by side), I want it to take up the entire height of the header. To achieve this all I had to do was edit the _header.scss file:

Info

Only the relevant section in the _header.scss file. Edit to this, keep the rest.

/home/vector/devel/sreema_filaments/mkdocs-material/src/templates/assets/stylesheets/main/components/_header.scss
&__button {
  // General button styles
  padding: px2rem(8px);
  margin: px2rem(4px);

  // Logo-specific styles
  &.md-logo {
    padding: px2rem(0px);  // This OVERRIDES the above padding
    margin: 0 0 0 px2rem(4px);   // This OVERRIDES the above margin
  }
}

Resizing logo dynamically

The default way to add your site logo in mkdocs works well only if its single svg logo, but using it when you have multiple logos, in png format it just doesn't look right. Therefore I tried to get the logo to appear bigger when the title displayed in the bar is the site_title, and make it smaller when its showing the headings.

/home/vector/devel/sreema_filaments/mkdocs-material/src/templates/assets/javascripts/bundle.ts
import "focus-visible"

import {
  EMPTY,
  NEVER,
  Observable,
  Subject,
  defer,
  delay,
  filter,
  map,
  merge,
  mergeWith,
  shareReplay,
  switchMap
} from "rxjs"

import { configuration, feature } from "./_"
import {
  at,
  getActiveElement,
  getOptionalElement,
  requestJSON,
  setLocation,
  setToggle,
  watchDocument,
  watchKeyboard,
  watchLocation,
  watchLocationTarget,
  watchMedia,
  watchPrint,
  watchScript,
  watchViewport
} from "./browser"
import {
  getComponentElement,
  getComponentElements,
  mountAnnounce,
  mountBackToTop,
  mountConsent,
  mountContent,
  mountDialog,
  mountHeader,
  mountHeaderTitle,
  mountLogoSize,
  mountPalette,
  mountProgress,
  mountSearch,
  mountSearchHiglight,
  mountSidebar,
  mountSource,
  mountTableOfContents,
  mountTabs,
  watchHeader,
  watchMain
} from "./components"
import {
  SearchIndex,
  setupClipboardJS,
  setupInstantNavigation,
  setupVersionSelector
} from "./integrations"
import {
  patchEllipsis,
  patchIndeterminate,
  patchScrollfix,
  patchScrolllock
} from "./patches"
import "./polyfills"

/* ----------------------------------------------------------------------------
 * Functions - @todo refactor
 * ------------------------------------------------------------------------- */

/**
 * Fetch search index
 *
 * @returns Search index observable
 */
function fetchSearchIndex(): Observable<SearchIndex> {
  if (location.protocol === "file:") {
    return watchScript(
      `${new URL("search/search_index.js", config.base)}`
    )
      .pipe(
        // @ts-ignore - @todo fix typings
        map(() => __index),
        shareReplay(1)
      )
  } else {
    return requestJSON<SearchIndex>(
      new URL("search/search_index.json", config.base)
    )
  }
}

/* ----------------------------------------------------------------------------
 * Application
 * ------------------------------------------------------------------------- */

/* Yay, JavaScript is available */
document.documentElement.classList.remove("no-js")
document.documentElement.classList.add("js")

/* Set up navigation observables and subjects */
const document$ = watchDocument()
const location$ = watchLocation()
const target$   = watchLocationTarget(location$)
const keyboard$ = watchKeyboard()

/* Set up media observables */
const viewport$ = watchViewport()
const tablet$   = watchMedia("(min-width: 960px)")
const screen$   = watchMedia("(min-width: 1220px)")
const print$    = watchPrint()

/* Retrieve search index, if search is enabled */
const config = configuration()
const index$ = document.forms.namedItem("search")
  ? fetchSearchIndex()
  : NEVER

/* Set up Clipboard.js integration */
const alert$ = new Subject<string>()
setupClipboardJS({ alert$ })

/* Set up progress indicator */
const progress$ = new Subject<number>()

/* Set up instant navigation, if enabled */
if (feature("navigation.instant"))
  setupInstantNavigation({ location$, viewport$, progress$ })
    .subscribe(document$)

/* Set up version selector */
if (config.version?.provider === "mike")
  setupVersionSelector({ document$ })

/* Always close drawer and search on navigation */
merge(location$, target$)
  .pipe(
    delay(125)
  )
    .subscribe(() => {
      setToggle("drawer", false)
      setToggle("search", false)
    })

/* Set up global keyboard handlers */
keyboard$
  .pipe(
    filter(({ mode }) => mode === "global")
  )
    .subscribe(key => {
      switch (key.type) {

        /* Go to previous page */
        case "p":
        case ",":
          const prev = getOptionalElement<HTMLLinkElement>("link[rel=prev]")
          if (typeof prev !== "undefined")
            setLocation(prev)
          break

        /* Go to next page */
        case "n":
        case ".":
          const next = getOptionalElement<HTMLLinkElement>("link[rel=next]")
          if (typeof next !== "undefined")
            setLocation(next)
          break

        /* Expand navigation, see https://bit.ly/3ZjG5io */
        case "Enter":
          const active = getActiveElement()
          if (active instanceof HTMLLabelElement)
            active.click()
      }
    })

/* Set up patches */
patchEllipsis({ viewport$, document$ })
patchIndeterminate({ document$, tablet$ })
patchScrollfix({ document$ })
patchScrolllock({ viewport$, tablet$ })

/* Set up header and main area observable */
const header$ = watchHeader(getComponentElement("header"), { viewport$ })
const main$ = document$
  .pipe(
    map(() => getComponentElement("main")),
    switchMap(el => watchMain(el, { viewport$, header$ })),
    shareReplay(1)
  )

/* Set up control component observables */
const control$ = merge(

  /* Consent */
  ...getComponentElements("consent")
    .map(el => mountConsent(el, { target$ })),

  /* Dialog */
  ...getComponentElements("dialog")
    .map(el => mountDialog(el, { alert$ })),

  /* Color palette */
  ...getComponentElements("palette")
    .map(el => mountPalette(el)),

  /* Progress bar */
  ...getComponentElements("progress")
    .map(el => mountProgress(el, { progress$ })),

  /* Search */
  ...getComponentElements("search")
    .map(el => mountSearch(el, { index$, keyboard$ })),

  /* Repository information */
  ...getComponentElements("source")
    .map(el => mountSource(el))
)

/* Set up content component observables */
const content$ = defer(() => merge(

  /* Announcement bar */
  ...getComponentElements("announce")
    .map(el => mountAnnounce(el)),

  /* Content */
  ...getComponentElements("content")
    .map(el => mountContent(el, { viewport$, target$, print$ })),

  /* Search highlighting */
  ...getComponentElements("content")
    .map(el => feature("search.highlight")
      ? mountSearchHiglight(el, { index$, location$ })
      : EMPTY
    ),

  /* Header */
  ...getComponentElements("header")
    .map(el => mountHeader(el, { viewport$, header$, main$ })),

  /* Header title */
  ...getComponentElements("header-title")
    .map(el => mountHeaderTitle(el, { viewport$, header$ })),

  /* Header logo */  // <-- Add this
  ...getComponentElements("logo")
    .map(el => mountLogoSize(el, { viewport$, header$ })),

  /* Sidebar */
  ...getComponentElements("sidebar")
    .map(el => el.getAttribute("data-md-type") === "navigation"
      ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))
      : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))
    ),

  /* Navigation tabs */
  ...getComponentElements("tabs")
    .map(el => mountTabs(el, { viewport$, header$ })),

  /* Table of contents */
  ...getComponentElements("toc")
    .map(el => mountTableOfContents(el, {
      viewport$, header$, main$, target$
    })),

  /* Back-to-top button */
  ...getComponentElements("top")
    .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))
))

/* Set up component observables */
const component$ = document$
  .pipe(
    switchMap(() => content$),
    mergeWith(control$),
    shareReplay(1)
  )

/* Subscribe to all components */
component$.subscribe()

/* ----------------------------------------------------------------------------
 * Exports
 * ------------------------------------------------------------------------- */

window.document$  = document$          /* Document observable */
window.location$  = location$          /* Location subject */
window.target$    = target$            /* Location target observable */
window.keyboard$  = keyboard$          /* Keyboard observable */
window.viewport$  = viewport$          /* Viewport observable */
window.tablet$    = tablet$            /* Media tablet observable */
window.screen$    = screen$            /* Media screen observable */
window.print$     = print$             /* Media print observable */
window.alert$     = alert$             /* Alert subject */
window.progress$  = progress$          /* Progress indicator subject */
window.component$ = component$         /* Component observable */
/home/vector/devel/sreema_filaments/mkdocs-material/src/templates/partials/header.html
<!-- Determine classes -->
{% set class = "md-header" %}
{% if "navigation.tabs.sticky" in features %}
  {% set class = class ~ " md-header--shadow md-header--lifted" %}
{% elif "navigation.tabs" not in features %}
  {% set class = class ~ " md-header--shadow" %}
{% endif %}

<!-- Header -->
<header class="{{ class }}" data-md-component="header">
  <nav
    class="md-header__inner md-grid"
    aria-label="{{ lang.t('header') }}"
  >

    <!-- Link to home -->
    <a
      href="{{ config.extra.homepage | d(nav.homepage.url, true) | url }}"
      title="{{ config.site_name | e }}"
      class="md-header__button md-logo"
      aria-label="{{ config.site_name }}"
      data-md-component="logo"
    >
      {% include "partials/logo.html" %}
    </a>

    <!-- Button to open drawer -->
    <label class="md-header__button md-icon" for="__drawer">
      {% set icon = config.theme.icon.menu or "material/menu" %}
      {% include ".icons/" ~ icon ~ ".svg" %}
    </label>

    <!-- Header title -->
    <div class="md-header__title" data-md-component="header-title">
      <div class="md-header__ellipsis">
        <div class="md-header__topic">
          <span class="md-ellipsis">
            {{ config.site_name }}
          </span>
        </div>
        <div class="md-header__topic" data-md-component="header-topic">
          <span class="md-ellipsis">
            {% if page.meta and page.meta.title %}
              {{ page.meta.title }}
            {% else %}
              {{ page.title }}
            {% endif %}
          </span>
        </div>
      </div>
    </div>

    <!-- Color palette toggle -->
    {% if config.theme.palette %}
      {% if not config.theme.palette is mapping %}
        {% include "partials/palette.html" %}
      {% endif %}
    {% endif %}

    <!-- User preference: color palette -->
    {% if not config.theme.palette is mapping %}
      {% include "partials/javascripts/palette.html" %}
    {% endif %}

    <!-- Site language selector -->
    {% if config.extra.alternate %}
      {% include "partials/alternate.html" %}
    {% endif %}

    <!-- Button to open search modal -->
    {% if "material/search" in config.plugins %}
      {% set search = config.plugins["material/search"] | attr("config") %}

      <!-- Check if search is actually enabled - see https://t.ly/DT_0V -->
      {% if search.enabled %}
        <label class="md-header__button md-icon" for="__search">
          {% set icon = config.theme.icon.search or "material/magnify" %}
          {% include ".icons/" ~ icon ~ ".svg" %}
        </label>

        <!-- Search interface -->
        {% include "partials/search.html" %}
      {% endif %}
    {% endif %}

    <!-- Repository information -->
    {% if config.repo_url %}
      <div class="md-header__source">
        {% include "partials/source.html" %}
      </div>
    {% endif %}
  </nav>

  <!-- Navigation tabs (sticky) -->
  {% if "navigation.tabs.sticky" in features %}
    {% if "navigation.tabs" in features %}
      {% include "partials/tabs.html" %}
    {% endif %}
  {% endif %}
</header>
/home/vector/devel/sreema_filaments/mkdocs-material/src/templates/assets/javascripts/components/header/logo/index.ts
import {
  EMPTY,
  Observable,
  Subject,
  defer,
  distinctUntilKeyChanged,
  finalize,
  map,
  tap
} from "rxjs"

import {
  Viewport,
  getElementSize,
  getOptionalElement,
  watchViewportAt
} from "~/browser"

import { Component } from "../../_"
import { Header } from "../_"

/* ----------------------------------------------------------------------------
 * Types
 * ------------------------------------------------------------------------- */

/**
 * Header
 */
export interface HeaderTitle {
  active: boolean                      /* Header title is active */
}

/* ----------------------------------------------------------------------------
 * Helper types
 * ------------------------------------------------------------------------- */

/**
 * Watch options
 */
interface WatchOptions {
  viewport$: Observable<Viewport>      /* Viewport observable */
  header$: Observable<Header>          /* Header observable */
}

/**
 * Mount options
 */
interface MountOptions {
  viewport$: Observable<Viewport>      /* Viewport observable */
  header$: Observable<Header>          /* Header observable */
}

/* ----------------------------------------------------------------------------
 * Functions
 * ------------------------------------------------------------------------- */

/**
 * Watch header title
 *
 * @param el - Heading element
 * @param options - Options
 *
 * @returns Header title observable
 */
export function watchHeaderTitle(
  el: HTMLElement, { viewport$, header$ }: WatchOptions
): Observable<HeaderTitle> {
  return watchViewportAt(el, { viewport$, header$ })
    .pipe(
      map(({ offset: { y } }) => {
        const { height } = getElementSize(el)
        return {
          active: y >= height
        }
      }),
      distinctUntilKeyChanged("active")
    )
}

/**
 * Mount header title
 *
 * This function swaps the header title from the site title to the title of the
 * current page when the user scrolls past the first headline.
 *
 * @param el - Header title element
 * @param options - Options
 *
 * @returns Header title component observable
 */
export function mountLogoSize(
  el: HTMLElement, options: MountOptions
): Observable<Component<HeaderTitle>> {
  return defer(() => {
    const push$ = new Subject<HeaderTitle>()
    push$.subscribe({

      /* Handle emission */
      next({ active }) {
        el.classList.toggle("md-header__title--active", active)

        // Add this line to also toggle the logo compact state
        const logo = document.querySelector(".md-header__button.md-logo")
        if (logo) {
          logo.classList.toggle("md-header__logo--compact", active)
        }
      },

      /* Handle complete */
      complete() {
        el.classList.remove("md-header__title--active")

        // Add this line to clean up the logo state on component destruction
        const logo = document.querySelector(".md-header__button.md-logo")
        if (logo) {
          logo.classList.remove("md-header__logo--compact")
        }
      }
    })

    /* Obtain headline, if any */
    const heading = getOptionalElement(".md-content h1")
    if (typeof heading === "undefined")
      return EMPTY

    /* Create and return component */
    return watchHeaderTitle(heading, options)
      .pipe(
        tap(state => push$.next(state)),
        finalize(() => push$.complete()),
        map(state => ({ ref: el, ...state }))
      )
  })
}

Site title transition

// Header title
&__title {
  flex-grow: 1;
  height: px2rem(48px);
  margin-inline: px2rem(20px) px2rem(8px);
  font-size: px2rem(28px);
  line-height: px2rem(28px);

  // Add transition for line-height changes
  transition: line-height 400ms cubic-bezier(0.1, 0.7, 0.1, 1);

  // Header title in active state, i.e. page title is visible
  &--active .md-header__topic {
    z-index: -1;
    pointer-events: none;
    opacity: 0;
    transition:
      transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),
      opacity   150ms;
    transform: translateX(px2rem(-25px));

    [dir="rtl"] & {
      transform: translateX(px2rem(25px));
    }

    + .md-header__topic {
      line-height: px2rem(48px);
      z-index: 0;
      font-size: px2rem(18px);
      pointer-events: initial;
      opacity: 1;
      transition:
        transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),
        opacity   150ms,
        line-height 400ms cubic-bezier(0.1, 0.7, 0.1, 1); // Add line-height transition here too
      transform: translateX(0);
    }
  }

  > .md-header__ellipsis {
    position: relative;
    width: 100%;
    height: 100%;
  }
}

Comments