# Dashboard customization

This article mainly describes the HTML structure of Kanka dashboards and offers some guidance and tips about customizing them. Since most widget types have small differences in their markup, it can also help you plan ahead without having to create demos for each type and examine them on a live page.

## Layout

Before we dive into the various widget types, it’s important to know how a dashboard is laid out. It consists of two distinct areas: the campaign header at the top (`div.campaign-header`) and the content area (`section.content`), the latter containing all dashboard widgets and a final div for the Dashboard Settings button. Here is a (simplified) general overview of the page’s HTML structure:

{% code overflow="wrap" %}

```html
<div id="campaign-dashboard" class="content-wrapper">
  <!-- Campaign header begins -->
  <div class="campaign-header campaign-imaged-header ...">
    <!-- See Campaign Header section below for full markup -->
  </div>
  <!-- Widgets section begins -->
  <section class="content">
    <div class="max-w-7xl mx-auto">
      <div class="dashboard-widgets grid grid-cols-12 ...">
        <!-- Widgets; details below -->
        ...
      </div>
      <!-- Extra row for Settings button -->
      <div class="text-center mt-6">
        <a href="https://app.kanka.io/w/1/dashboard-setup" class="btn2 btn-lg btn-primary" title="Dashboard Setup">
          <i class="fa-solid fa-cog " aria-hidden="true"></i>
          Dashboard Setup
        </a>
      </div>
    </div>
  </section>
</div>
```

{% endcode %}

### Campaign Header

The campaign header is mandatory on the default dashboard, and optional on additional dashboards. Its various containers control the background image for the whole section, the background for the campaign presentation, the campaign’s title and the introduction text set in the campaign’s settings under the **Dashboard** tab. On someone else’s campaign, `div.action-bar` contains a link to follow/unfollow the campaign (so it's a good idea to test your campaign in an incognito window or while impersonating another user if you make heavy changes in this area). Users with sufficient access rights also see a dropdown menu there that leads to other dashboards and various options.

{% code overflow="wrap" %}

```html
  <div class="campaign-header campaign-imaged-header ..." style="background-image: url(https://th.kanka.io/.../image.png)">
    <div class="campaign-header-content ...">
      <div class="campaign-content">
        <div class="campaign-head ...">
          <div class="grow">
            <a href="https://app.kanka.io/w/1/overview" title="Campaign Name" class="campaign-title ...">Campaign Name</a>
          </div>
          <div class="action-bar ...">
            <!-- Follow button, if not looking at your own campaign -->
            <button id="campaign-follow" ...>
              <i class="fa-solid fa-star"></i>
              <span id="campaign-follow-text">Stop following</span>
            </button>
            <!-- Otherwise, action dropdown -->
            <div class="dropdown">
              <button class="btn2 btn-sm" data-dropdown="" aria-expanded="false" data-loaded="1">
                <i class="fa-solid fa-ellipsis-h " aria-hidden="true"></i>
              </button>
              <div class="dropdown-menu hidden" role="menu">
                <!-- Dropdown links to other dashboards and various settings -->
              </div>
            </ul>
          </div>
        </div>
        <div class="preview">
          <!-- Contains the excerpt set in the campaign’s main settings, Dashboard tab -->
        </div>
      </div>
    </div>
  </div>
```

{% endcode %}

Even on non-Premium campaigns, a header image can be set as a backdrop to this entire area. If set, `div.campaign-header` gains the `.campaign-imaged-header` class, which means you can not only control the background’s display, but also style any child element of `div.campaign-header` based on the presence or absence of a cover image. For example, you could control the padding of `div.preview` based on the presence or absence of an image using `.campaign-imaged-header .preview {...}` and `.campaign-header:not(.campaign-imaged-header) .preview {...}` respectively.

### Widgets

Widgets come in a variety of types and sizes, each with dedicated classes that allow us to create specific rules based on both their content and their dimensions. You can also give individual widgets custom CSS classes in their **Advanced** tab, which can greatly simplify your selectors compared to using the size- and type-related classes as described below. For example, you could have a "blue-widget" class that gives those widgets a blue background. However, since some widget types have different HTML structures (detailed below), not all customizations will work across multiple types without some effort.

![Widget settings, Advanced tab](https://user-images.githubusercontent.com/1753500/213088599-e7f04e12-8eeb-4b9f-90ee-12ee8dccbbc7.png)

Note however that those custom classes aren't applied to the outermost layer of a widget, but to the second layer (referred to as "secondary container" in the [widget type table below](#widget-types-wip)). In most cases, that shouldn't hinder styling since all content is under the secondary layer, but if you aim to make more drastic layout changes that require grabbing the whole widget, you may need to use the [:has()](https://developer.mozilla.org/en-US/docs/Web/CSS/:has) pseudo-class, for example `div.widget:has(.blue-widget) {...}`.

### Widget size and layout

On the dashboard, widgets are laid out using a [CSS grid](https://css-tricks.com/snippets/css/complete-guide-grid/), which gives us a lot of flexibility and control. When set up, each row can contain from 1 to 4 widgets depending on their size, and their positions are set via drag-and-drop. To accommodate the various possible widths, each row of widgets is broken down into 12 virtual columns "under the hood", and widgets occupy a certain number of those columns based on their defined width.

<figure><img src="/files/GeSgodG8Z3ppUVw5LOWl" alt="Dashboard grid highlight"><figcaption><p>Dashboard grid example: each Small (33%) widget occupies 4 of 12 columns (plus interstitial gaps)</p></figcaption></figure>

For example, a 100%-width widget occupies all 12 columns via the class `md:col-span-12`, while a 33%-width widget occupies 4 columns via `md:col-span-4`. Therefore, you can target all widgets of a given format with a rule such as `.md\:col-span-12 > .widget {...}`.

{% hint style="warning" %}
Colons in class names are an abomination *(yes, I know, they are valid HTML)* that requires "escaping" with a backslash character in CSS selectors. `.md:col-span-12 { ... }` is not valid CSS since colons indicate pseudo-classes and there is no "col-span-12" pseudo-class. Thanks, Tailwind 😤
{% endhint %}

The following table shows each available widget size by name, percentage and class.

| Format name          | Tiny       | Small      | Half       | Wide       | Large      | Full        |
| -------------------- | ---------- | ---------- | ---------- | ---------- | ---------- | ----------- |
| **Width percentage** | 25%        | 33%        | 50%        | 66%        | 75%        | 100%        |
| **Column class**     | col-span-3 | col-span-4 | col-span-6 | col-span-8 | col-span-9 | col-span-12 |

### Widget types

All widgets have the `div.widget` class and an additional class based on their type, plus a unique ID in the form of `widget-col-[id]`. The next layer specifies the widget’s subtype, with a matching ID in the form of `dashboard-widget-[id]`. Maps are differentiated on a third layer.

<table><thead><tr><th>Type and subtype</th><th>Primary container</th><th width="50">></th><th>Secondary container</th><th width="40">></th><th>Tertiary container</th></tr></thead><tbody><tr><td><strong>Entry preview</strong></td><td>div.widget.widget-preview</td><td>></td><td>div.widget-preview</td><td></td><td></td></tr><tr><td><strong>Entry preview (map)</strong></td><td>div.widget.widget-preview</td><td>></td><td>div.widget-preview</td><td>></td><td>div.widget-map</td></tr><tr><td><strong>Random entry preview</strong></td><td>div.widget.widget-random</td><td>></td><td>div.widget-random</td><td></td><td></td></tr><tr><td><strong>Calendar</strong></td><td>div.widget.widget-calendar</td><td>></td><td>div.widget-calendar</td><td></td><td></td></tr><tr><td><strong>Entry list (such as unmentioned or recently modified)</strong></td><td>div.widget.widget-recent</td><td>></td><td>div.widget-list</td><td></td><td></td></tr><tr><td><strong>Entry list (single entry preview)</strong></td><td>div.widget.widget-recent</td><td>></td><td>div.widget-preview</td><td></td><td></td></tr><tr><td><strong>Text header*</strong></td><td>div.widget.widget-header</td><td></td><td></td><td></td><td></td></tr></tbody></table>

{% hint style="danger" %}
**\*Heads up:** `.widget-header` is used both to identify Header-type widgets and on the header part of all widgets that have a header and a body. Therefore, `.widget.widget-header {...}` (with chained classes) and `.widget .widget-header {...}` (with space-separated classes) are deceptively different selectors: the first targets the outer container of Header widgets, while the second targets the header inside most widgets.
{% endhint %}

These classes give us the flexibility to precisely target any combination of types and formats, as well as specific widgets by ID or arbitrary groups of them (via custom classes). Because `.widget-preview` is so overused and the same classes can appear on two different levels, however, we need to be fairly specific with our selectors:

{% code overflow="wrap" %}

```css
/* Full-width entry previews, excluding maps: */
.md\:col-span-12 > .widget-preview:not(:has(.widget-map)) {...}

/* Calendars that are at least 50% width: */
:is(md\:col-span-12, md\:col-span-9, md\:col-span-8, md\:col-span-6) > .widget-calendar {...}

/* All widgets with our custom class except a specific one, once we have determined its ID: */
.custom-class-name:not(#dashboard-widget-1234) {...}
/* Note that custom classes are applied to the secondary container, NOT to the top-level div.widget, so it may not be suitable for resizing and other effects targeting the outermost container. For those, you can use the #widget-col-1234 ID or rely on the :has() pseudo-class. */
```

{% endcode %}

### Widget structure

All widgets except text headers, map previews and property previews use a similar content structure: a `div#widget-col-(id).widget.col-md-(1-12)` specifying the width and general type of widget, a `div#dashboard-widget-(id)` indicating the more specific subtype, and some variation of a header and a body. The header and body are structured according to a few different models which are detailed below.

#### List header

The simplest header is used on list widgets such as Unmentioned entries and Recently modified entries. It simply consists of an `h4.text-lg` with two spans:

```html
<!-- List widget - no link -->
<h4 class="text-lg mb-3 px-4 pt-4 flex gap-2">
  <span class="grow">
    Recently modified
  </span>
  <span class="flex-none flex gap-1"></span>
</h4>
```

However, when using a Recently modified list for a single entry category, the header will have a link instead of plain text, which may lead to inconsistent styling between lists if not accounted for:

{% code overflow="wrap" %}

```html
<!-- List widget - link to specific entry category -->
<h4 class="text-lg mb-3 px-4 pt-4 flex gap-2">
  <span class="grow">
    <a href="https://app.kanka.io/w/1234/events">Events - Entry list</a>
  </span>
  <span class="flex-none flex gap-1"></span>
</h4>
```

{% endcode %}

#### Entry link header

Also quite simple, entry previews have a `div.header` with a link. Private entries and dead characters have an extra span to denote their status with an icon:

{% code overflow="wrap" %}

```html
<!-- Entry preview - link & status icons -->
<div class="widget-header">
  <!-- This span is only present if the entry is private -->
  <span data-title="This entry is private and only visible to members of the campaign's Admin role." data-toggle="tooltip" data-html="true"><i class="fa-solid fa-lock " aria-hidden="true"></i></span>
  <!-- This is the actual link and title -->
  <a href="https://app.kanka.io/w/19153/entities/493914" class="flex gap-1 text-xl p-4 pb-0">
    <span class="grow">Dead Character</span>
    <!-- This span is only present if a dead character icon is needed -->
    <span data-title="Dead" data-toggle="tooltip" data-html="true"><i class="ra ra-skull " aria-hidden="true"></i></span>
  </a>
</div>
```

{% endcode %}

#### Entry image header

On entry previews that are set to use the entry’s image or header image (and where such image exists), the top structure is slightly different. The image is shown first separately, and the title underneath; both of them link to the entry:

{% code overflow="wrap" %}

```html
<div class="widget-header">
  <a href="https://app.kanka.io/w/.../entities/..." class="widget-image cover-background bg-center aspect-video rounded-t ">
    <picture class="entity-image-wide">
      <source srcset="https://th.kanka.io/....jpg" media="(min-width: 768px)">
      <img src="https://th.kanka.io/....jpg" class="w-full">
    </picture>
  </a>
  <a href="https://app.kanka.io/w/.../entities/..." class="flex gap-1 text-xl p-4 pb-0">
    <span class="grow">Entry Name</span>
  </a>
</div>
```

{% endcode %}

At the time of writing, the class `.entity-image-wide` on the picture element isn't actually used in Kanka's CSS, so most of your image styling would override `.widget-image` and other classes on the anchor instead.

#### No header

On map entry previews, there is no header at all, but it is possible to fake one with CSS ([example below](#add-a-title-to-a-map-widget)). There is however an extra layer before the actual content, with `div.widget-map` identifying the entry as a map.

#### Entry preview body (full)

When an entry preview is set to display its full description in the **Setup** tab, `.widget-body` is at its most simple and contains only the description’s content:

```html
<div class="widget-body p-4">
  <div class="entity-content">
    ...
  </div>
</div>
```

#### Entry preview body (default)

By default, only part of the entry’s description is visible, up to about 200 pixels in height, with a toggle at the bottom of the widget to expand it and show the full description. `.widget-body`’s first child gains a specific ID for toggling purposes: `#widget-preview-body-(id)`. This div contains `div.entity-content` and a second div with a gradient effect when collapsed. It is followed by the anchor that toggles expansion: `a.preview-switch`.

{% code overflow="wrap" %}

```html
<div class="widget-body p-4 ">
  <!-- max-h-52 is removed when the widget is expanded: -->
  <div class="preview overflow-hidden relative max-h-52" data-toggle="preview" id="widget-preview-body-1234">
    <div class="entity-content">
      ...
    </div>
    <!-- This gradient div gains 'display:none' when the widget is expanded: -->
    <div class="absolute w-full bottom-0 h-52 gradient-to-base-100">
    </div>
  </div>
  <a href="#" class="preview-switch inline-block w-full text-center" id="widget-preview-switch-1234" data-widget="1234" data-toggle="tooltip" data-title="Click to toggle">
    <i class="fa-solid fa-chevron-down" aria-hidden="true"></i>
    <span class="sr-only">Click to toggle</span>
  </a>
</div>
```

{% endcode %}

#### Entry preview body (with pinned relations, properties or group members)

The **Advanced** tab of widget settings allows you to include pinned relations or properties in the preview, and Family or Organization members. Doing so adds corresponding divs under the entry’s description, which allows you to style and position these blocks creatively independently from the rest of the content (*see* [*below*](#pinned-relations-as-a-sidebar) *for an example*):

{% code overflow="wrap" %}

```html
<div class="entity-content">
  ...
</div>
<!-- Optional Relations block -->
<div class="widget-advanced-relations">
  <dl class="dl-horizontal">
    <div class="pinned-relation flex gap-2 flex-wrap" data-target="1234" data-relation="Pionniers arcadiens" data-visibility="1" data-attitude="">
      <strong class="">Relation name</strong>
      <span class="grow text-right">
        <span>
          <a class="name" data-toggle="tooltip-ajax" data-id="1234" data-url="https://app.kanka.io/w/.../tooltip" href="https://app.kanka.io/w/..." data-loaded="1" aria-expanded="false">Relation target</a>
        </span>
      </span>
    </div>
    <!-- repeat div.pinned-relation, optionally with .relation-repeat added for identical relation types (e.g. "sibling") ... -->
  </dl>
</div>

<!-- Optional Properties block -->
<div class="widget-advanced-attributes">
  <ul class="m-0 p-0 list-none">
    <div class="pinned-attribute flex gap-2 flex-wrap " data-attribute="Name" data-target="1234">
      <strong>Property name</strong>
      <p class="text-right grow m-0 inline-block">Property value</p>
    </div>
    <!-- repeat div.pinned-attribute... -->
  </ul>
</div>

<!-- Optional group members block -->
<div class="widget-advanced-members">
  <div class="flex flex-col gap-2 members">
    <div class="grid grid-cols-2 gap-2 members" data-role="role" data-status="1">
      <div class="font-extrabold">Member role</div>
      <div>
        <span>
          <a class="name" data-toggle="tooltip-ajax" data-id="1234" data-url="https://app.kanka.io/w/.../tooltip" href="https://app.kanka.io/w/..." data-loaded="1" aria-expanded="false">Member name</a>
        </span>
      </div>
    </div>
    <!-- repeat div.members ... -->
  </div>
</div>
<div class="absolute w-full bottom-0 h-52 gradient-to-base-100">
</div>
```

{% endcode %}

#### List body (such as Recently modified and Unmentioned entries)

List widgets, unless set to show only the first result’s entry preview, have the `.widget-list` class on the second level. This in turns contains a simple h4 header and a `div.widget-recent-list` where the actual results appear. Up to ten results are shown in a single column (`div.flex-col`), each containing elements for the entry’s image, its name and a `div.blame` for the name of the last editor and the timestamp. Additionally, a link in `div.text-center > a.widget-recent-more` loads the next set of results, where applicable.

{% code overflow="wrap" %}

```html
<div class="widget-recent-list overflow-auto px-4 pb-4 max-h-[400px]">
  <div class="flex flex-col gap-2">
    <div class="flex items-center gap-2">
      <!-- Entry thumbnail as a clickable background image -->
      <a class="entity-picture inline-block rounded-full cover-background w-9 h-9 flex-shrink-0" style="background-image: url('https://....cloudfront.net/images/defaults/journals_thumb.jpg');" title="Entry Title" href="https://app.kanka.io/w/..."></a>
      <div class="grow break-all">
        <span>
          <a class="name" data-toggle="tooltip-ajax" data-id="1234" data-url="https://app.kanka.io/w/.../tooltip" href="https://app.kanka.io/w/..." data-loaded="1" aria-expanded="false">Entry Title</a>
        </span>
        <!-- If private: -->
        <i class="fa-solid fa-lock" title="This entry is private and only visible to members of the campaign's Admin role." aria-hidden="true"></i>
        <span class="sr-only">This entry is private and only visible to members of the campaign's Admin role.</span>
      </div>
      <div class="blame flex-none text-right text-xs">
        <span class="author block">Author’s name</span>
        <span class="elapsed text-neutral-content text-xs" title="2023-06-16 03:20:18 UTC">7 months ago</span>
      </div>
    </div>
    <!-- Repeat up to 10 times -->
    <div class="text-center">
      <a href="#" class="widget-recent-more" data-url="https://app.kanka.io/w/.../dashboard/widgets/recent/...?page=2">
        <span class="inline-block p-3">Next</span>
        <!-- Spinner animation while loading results; display: none afterwards -->
        <i class="fa-solid fa-spinner fa-spin spinner" style="display: none;" aria-hidden="true"></i>
      </a>
    </div>
  </div>
  <!-- Additional results are appended here in a new .flex-col -->
</div>
```

{% endcode %}

{% hint style="info" %}
Note that when you load additional results, they are added after the current "Next" link with a new `"Next"`of their own. However, the old "Next" div still exists and is simply emptied. Therefore, even though it is normally invisible due to being empty, you may see undesirable effects between sets of results if you style that div rather than the link directly. This can be avoided using the :has pseudo-class to check whether a link is present:

```css
.widget-recent-list .text-center:has(a) {
	border: 1px solid blue;
}
```

Conversely, you could make use of that distinction to add visual separation between batches of results without affecting the current link, with a rule such as:

```css
.widget-recent-list .text-center:not(:has(a)) {
	border-top: 1px dashed slategrey;
	margin-bottom: 5px;
}
```

{% endhint %}

#### Calendar body

For calendars, the header is followed by a non-specific `div.p-4`, which itself contains a `div.widget-loading` for a spinning animation, followed by `div#widget-body-(id)` for the actual content. Let’s look at the subheader at the top first, showing the current date with back and forward buttons:

{% code overflow="wrap" %}

```html
<!-- While loading, show a spinner, then display: none -->
<div class="text-center py-10 text-2xl" id="widget-loading-12345" style="display: none;">
  <i class="fa-solid fa-spinner fa-spin " aria-hidden="true"></i>
</div>
<div id="widget-body-12345">
  <div class=" grid gap-4 w-full grid-cols-1 md:grid-cols-2 ">
    <!-- Subheader with current day and back/forward buttons-->
    <div class="col-span-2 current-date text-center text-xl flex items-center justify-center gap-2" id="widget-date-12345">
      <a href="#" class="widget-calendar-switch" data-url="https://app.kanka.io/w/1234/dashboard/widgets/calendar/12345/sub" data-widget="12345" data-toggle="tooltip" data-title="Change date to previous day" role="button">
        <i class="fa-solid fa-chevron-circle-left" aria-hidden="true"></i>
        <span class="sr-only">Change date to previous day</span>
      </a>
      <span>3 February, 9001 </span>
      <a href="#" class="widget-calendar-switch" data-url="https://app.kanka.io/w/1234/dashboard/widgets/calendar/12345/add" data-widget="12345" data-toggle="tooltip" data-title="Change date to next day" role="button">
        <i class="fa-solid fa-chevron-circle-right" aria-hidden="true"></i>
        <span class="sr-only">Change date to next day</span>
      </a>
    </div>
    ...
```

{% endcode %}

`.current-date` is a good target for decorating this section as a whole, whereas `.current-date > span` would allow you to style the date itself without affecting the screen reader hints (`.sr-only`).

This is followed by two columns of up to 5 past and 5 upcoming events:

{% code overflow="wrap" %}

```html
    ...
    <!-- Previous events column -->
    <div class="flex flex-col gap-2 ">
      <div class="text-lg">
        Previous
        <a href="//docs.kanka.io/en/latest/guides/dashboard.html#known-limitations" target="_blank" data-toggle="tooltip" data-title="Why are these reminders being shown?">
          <i class="fa-solid fa-question-circle " aria-hidden="true"></i>
          <span class="sr-only">Why are these reminders being shown?</span>
        </a>
      </div>
      <ul class="style-none p-0">
        <li data-ago="41" class="flex gap-2">
          <div class="grow">
            <a href="https://app.kanka.io/w/1234/entities/2976043">Event Name I</a>
          </div>
          <div class="flex gap-1 items-center">
            <!-- If the reminder has a Comment filled in, an icon can be hovered to read it: -->
            <i class="fa-solid fa-comment" data-title="Reminder comment is feeling talkative" data-toggle="tooltip" data-placement="bottom" aria-hidden="true"></i>
            <!-- Hovering the calendar icon shows the event’s date: -->
            <i class="fa-solid fa-calendar" data-title="18 May, 9000 " data-toggle="tooltip" data-placement="bottom" aria-hidden="true"></i>
          </div>
        </li>
          <div class="grow">
            <a href="https://app.kanka.io/w/1234/entities/3313789">Event Name II</a>
          </div>
          <div class="flex gap-1 items-center">
             <!-- If the reminder is a recurring event, a circular arrow icon indicates it: -->
            <i class="fa-solid fa-arrows-rotate" data-title="Recurring" data-toggle="tooltip" aria-hidden="true"></i>
            <i class="fa-solid fa-calendar" data-title="12 May, 9000 " data-toggle="tooltip" data-placement="bottom" aria-hidden="true"></i>
          </div>
        </li>
        <!-- Up to 5 events total -->
      </ul>
    </div>
    <!-- Upcoming events column -->
    <div class="flex flex-col gap-2 ">
    <!-- More of the same -->
    </div>
  </div>
</div>
```

{% endcode %}

You could for example target both columns with `.widget-calendar .flex-col {...},` or only one of the two by adding `:first-child` or `nth-child(2)` respectively; or style all reminder links with `.widget-calendar ul a {...}` without affecting other links in the widget.

#### Map preview body

Map previews are too complex to be explored in detail here, but a `div.widget-map` replaces the typical header and body combination and contains a single `div#map(id)` with a slew of classes and inline styles. Because of these inline styles, resizing is best done directly on this element by referencing it by ID and using the `!important` keyword. A few other potentially interesting elements for styling are included in the sample below, namely the Explore button, `.leaflet-map-pane` which contains the underlying map, and `.leaflet-control-container` which contains controls such as the zoom buttons and layer selector.

{% code overflow="wrap" %}

```html
<div class="widget-map">
  <div class="map map-dashboard leaflet-container leaflet-touch leaflet-fade-anim leaflet-grab leaflet-touch-drag leaflet-touch-zoom" id="map1315" style="width: 100%; height: 100%; position: relative;" tabindex="0">
    <a href="https://app.kanka.io/w/.../explore" target="_blank" class="btn2 btn-primary btn-xs btn-map-explore z-[820] absolute bottom-3 right-3">
      <i class="fa-regular fa-map " aria-hidden="true"></i>
      Explore
    </a>
    <div class="leaflet-pane leaflet-map-pane" style="transform: translate3d(7px, 0px, 0px);">
      ...
    </div>
    <div class="leaflet-control-container">
      ...
    </div>
  </div>
</div>
```

{% endcode %}

#### Text header

Text headers are simply a heading (your choice from h1 to h6), optionally wrapped in an anchor if a target entry is provided. These two (or three) layers allow some flexibility regarding borders, backgrounds and padding, in addition to formatting the text, which is very plain by default.

{% code overflow="wrap" %}

```html
<div class="col-span-12 md:col-span-12 widget widget-header" id="widget-col-205449">
  <a href="https://app.kanka.io/w/.../entities/...">
    <h3 class="widget-header-text text-center my-4 custom-class" id="dashboard-widget-205449">
      Header text
    </h3>
  </a>
</div>
```

{% endcode %}

It’s worth noting that a header’s width can be set just like other widgets, so you can get creative with side-by-side blocks. Though for more complex designs, it may be easier to use an entry preview and hide its header, relying solely on the entry’s content for your layout.

Also note that since custom classes added to this type of widget are placed directly on the heading element (for example `custom-class` above), any outer styling would have to rely on the `:has` pseudo-class or the widget’s outer ID.

#### Properties iframe

If you choose the Properties option in the Display dropdown of an entry preview, the relevant part of the entry’s Properties page will be injected as an iframe element. This can display either a raw list of properties as dl/dt elements, or a fully customized Plugin Library character sheet if one is set for that entry.

{% code overflow="wrap" %}

```html
<div class="widget-body p-0 ">
  <iframe src="https://app.kanka.io/w/1/entities/1234/attributes-dashboard" class="entity-attributes w-full"></iframe>
</div>
```

{% endcode %}

It’s worth noting that iframes are treated by the browser as separate documents. As such, you cannot influence the appearance of an iframe's content with a rule like `#campaign-dashboard iframe .wrapper {...}` — the dashboard’s CSS doesn’t see anything past the iframe in such a selector. However, [container queries](/kanka-cookbook/css/adapting-layout-to-context.md) can come in handy to help you write width-based rules rather than parent-based ones, and thus make sure your properties display properly in a narrow widget.

## Widget customization examples

### Pinned relations as a sidebar

Here is a simple bit of CSS I use on my session log (*Entry list widget, in preview mode showing the latest Journal*) to turn pinned relations into a sidebar, similar to pins on the entry itself. I use it to show the session’s participating player characters, along with a link to the previous session log. Additional styling can be used, but I am only showing the positioning and spacing properties below to get you started.

![Session log with pinned relations as sidebar](/files/QWx7oYCwgSdljNGD3wC6)

```css
/* Turn the content area into a 2-column grid */
#widget-preview-body-52696 {
	margin-top: 15px;
	display: grid;
	grid-template-columns: auto minmax(200px, 20%);
}
/* Justify entity-content for a cleaner look */
#widget-preview-body-52696 .entity-content {
	text-align: justify;
}
/* Make a cleaner separation between relation names and items */
#widget-preview-body-52696 .pinned-relation strong {
	display: block;
}
```

### Add a title to a map widget

For those wanting a consistent look across their widgets, the absence of a header on map previews can be annoying. Although we can’t create a link to the entry, we can at least fake a title using a pseudo-element (this requires targeting each map individually, since we have to specify its name). Here is an example mimicking the default style:

{% code overflow="wrap" %}

```css
/* Put the name in a ::before pseudo-element, reusing styles from standard headers */
.widget-map::before {
	display: block;
	padding: 1rem;
	font-size: 1.25rem;
	color: var(--header-text, hsl(var(--bc)/var(--tw-text-opacity)));
	line-height: 1.75rem;
}
#dashboard-widget-44291::before {
	content: "Arcadie";
}
```

{% endcode %}

And one with a background image (legibility could be better and a border might be nice, but this covers the basics):

{% code overflow="wrap" %}

```css
.widget-map::before {
	display: block;
	padding: 2rem 1rem;
	font-size: 1.25rem;
	color: var(--header-text, hsl(var(--bc)/var(--tw-text-opacity)));
	text-shadow: rgba(0,0,0,.9) 3px 3px 4px;
	line-height: 1.75rem;
	background-size: cover;
	background-repeat: no-repeat;
	border-top-left-radius: .25rem;
	border-top-right-radius: .25rem;
}
/* Customize the image source and positioning for each widget, but also corresponding text properties to ensure legibility */
#dashboard-widget-44291::before {
	content: "Arcadie";
	background-image: url("https://th.kanka.io/.../image.jpg");
	background-position: 50% 10%;
	color: white;
	text-shadow: rgba(0,0,0,.9) 3px 3px 4px;
}
```

{% endcode %}

![Pseudo map widget banner header](/files/GJmIy5bwtFONgMVEx2Fe)

#### Custom map preview height

Fairly often, I get people asking how to make maps taller on the dashboard since the default size isn’t really suited to map visualization. Fortunately, that’s a very easy fix as long as you use `!important` to override the inline height definition:

```css
.map-dashboard {
	height: 400px !important;
}
```

## Custom dashboards

You can create additional dashboards, which can be targeted via a class on the body tag, `.dashboard-(id)`. Note that the default dashboard is not similarly identified, so you have to resort to something like `body:not([class*="dashboard-"]) #campaign-dashboard {...}` to target it while excluding your custom dashboards, or `body:not(.dashboard-2):not(.dashboard-3) #campaign-dashboard {...}` to exclude specific ones.

On custom dashboards, the Campaign Header is optional.

### Displaying a unique Campaign Header on custom dashboards

Showing the Campaign Header with the same introductory text on every custom dashboard may seem a little boring or unnecessary, but you may still want to include it for its unique aesthetics. Since custom dashboards have a unique ID, we can use it creatively to insert more customized content in this special, full-width section, following a few (admittedly convoluted) steps.

First, edit your campaign Excerpt in the campaign editor (**Dashboard** tab) and, in Code View, enclose the content you want to display in your main Campaign Header in a unique div (for example `<div id="default-excerpt">default header content</div>`). This will let you control whether your main presentation should be displayed on each dashboard.

Next, add additional content to your Excerpt in a different div, also giving it a unique ID, such as `<div id="excerpt-1">custom dashboard 1 header content</div>`. You can repeat this process multiple times for several dashboards.

Once your excerpts are ready, add the "Campaign header" widget to each dashboard (a special type that isn’t offered on the default dashboard) and make note of each dashboard’s ID in the address bar (which should end in `?dashboard=<id>`).

Once your excerpts and dashboards are mapped out, all you need to do is hide every excerpt by default, then make each one visible on the corresponding dashboard:

{% code overflow="wrap" %}

```css
/* Hide all variant excerpts everywhere by default for simplicity.
 * The attribute selector uses |= to match all IDs starting with "excerpt" followed by a dash.
 * Also hide the default excerpt on all custom dashboards. */
div[id|="excerpt"],
body[class*="dashboard-"] #default-excerpt {
	display: none;
}
/* Reset visibility on alternate excerpts by matching dashboard IDs to excerpt IDs */
.dashboard-132 #excerpt-2,
.dashboard-321 #excerpt-3 {
	display: initial;
}
```

{% endcode %}

Beyond these variant excerpts, you can of course also customize the appearance of each Campaign Header by targeting `.campaign-header`, `.campaign-content`, etc. as appropriate to change the background image source, display a different title or none, etc.

<figure><img src="https://storage.ko-fi.com/cdn/generated/zfskfgqnf/rest-cdd7684ae7838a2edd4a643c9403151f-suapruoq.jpg" alt="Support me on Ko-fi" width="375"><figcaption></figcaption></figure>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://salvatos.gitbook.io/kanka-cookbook/css/dashboard-customization.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
