-
Notifications
You must be signed in to change notification settings - Fork 0
[CSS] Dashboard customization
This article mainly describes the HTML structure of Kanka dashboards and offers some guidance and tips about customizing it. 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.
Before we dive into the various widget types, it’s important to know how the 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 rows of dashboard widgets (div.row
). There is also a final row at the bottom that holds the Dashboard Settings button. Here is a general overview of the page’s HTML structure:
<div class="content-wrapper" id="campaign-dashboard" style="min-height: 240.967px;">
<!-- Campaign header begins -->
<div class="campaign-header campaign-imaged-header" style="background-image: url(https://images.kanka.io/user/....jpg) ">
/* See Campaign Header section below for full markup */
</div>
<!-- Widget section begins -->
<section class="content">
<div class="dashboard-widgets">
<div class="row">
<!-- Rows of widgets -->
</div>
...
</div>
<!-- Extra row for Settings button -->
<div class="row margin-top">
<div class="col-md-12 text-center">
<a href="https://kanka.io/en-US/campaign/.../dashboard-setup" class="btn btn-default" title="Dashboard Settings">
<i class="fa-solid fa-cog"></i> Dashboard Settings </a>
</div>
</div>
</section>
</div>
Widgets are placed on rows (section.content > div.row
) that can contain from 1 to 4 widgets. Their positions can be set in the Dashboard Settings page via drag-and-drop. To accommodate for the various possible widths, each row is broken down into 12 virtual columns, and widgets occupy a certain number of columns based on their defined width. For example, a 100%-width widget occupies all 12 columns via the class div.col-md-12
, while a 50%-width widget occupies 6 columns via div.col-md-6
. Therefore, you can target all widgets of a given format with a rule such as .col-md-12 > .widget {...}
.
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-md-3 | col-md-4 | col-md-6 | col-md-8 | col-md-9 | col-md-12 |
It’s worth noting that those classes are ultimately used in CSS to determine the percentage of available width used (e.g. .col-md-12 { width: 100%; }
). You can therefore easily change the relative width of those classes. For example, a simple .dashboard-widgets .col-md-12 { width: 80%; }
would make all of your "Full" widgets use only 80% of available width, leaving a heavy margin on the sides. Just keep in mind that this virtual column class system is used throughout Kanka, so make sure that your selectors are specific enough.
The campaign header is mandatory on the default dashboard, and optional on additional dashboards. The 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. Users with sufficient access rights also see a dropdown menu there that leads to other dashboards and various options.
<div class="campaign-header campaign-imaged-header" style="background-image: url(https://images.kanka.io/user/....jpg) ">
<div class="campaign-header-content">
<div class="campaign-content">
<div class="campaign-head">
<a href="https://kanka.io/en-US/campaign/.../campaigns/..." title="Campaign Name" class="campaign-title"> Campaign Name </a>
<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>
<ul class="dropdown-menu dropdown-menu-right">
<!-- Dropdown links to other dashboards and various settings -->
</ul>
</div>
</div>
<div class="preview">
<!-- Contains the excerpt set in the campaign’s general Dashboard settings -->
</div>
</div>
</div>
</div>
Even on non-boosted 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 with .campaign-imaged-header .preview {...}
and .campaign-header:not(.campaign-imaged-header) .preview {...}
respectively.
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. Since Kanka 1.22, you can also give individual widgets custom CSS classes from 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.
Additionally, each widget has a unique ID in the form of dashboard-widget-12345
which you can find by inspecting the container, so you can design a very precise layout and give any single widget a distinct look. Of course, with the ability to add custom classes via the interface, you could also assign each widget a unique class instead of looking up the ID assigned by Kanka.
All widgets have the div.widget
class and an additional class based on their type. This div contains another div that specifies the widget’s ID (in the form dashboard-widget-[id]
) as well as the .panel
class and more specific widget subtypes.
Type and subtype | Widget classes | > | Panel classes |
---|---|---|---|
Entity preview | div.widget.widget-preview | > | div.panel.panel-default.widget-preview |
Entity preview (map) | div.widget.widget-preview | > | div.panel.panel-default.widget-preview.widget-map |
Random entity | div.widget.widget-random | > | div.panel.panel-default.widget-preview |
Calendar | div.widget.widget-calendar | > | div.panel.panel-default.widget-render |
Entity list (including unmentioned or recently modified) | div.widget.widget-recent | > | div.panel.panel-default > div.panel-body.widget-recent-list |
Entity list (single entity preview) | div.widget.widget-recent | > | div.panel.panel-default > div.panel-body.widget-recent-body |
Text header | div.widget.widget-header | — | N/A |
This gives us the flexibility to precisely target any combination of types and formats, but also specific rows and widgets:
/* Full-width entity previews, excluding maps: */
.col-md-12 > .widget-preview > .widget-preview:not(.widget-map) {...}
/* Calendars that are at least 50% width: */
:is(.col-md-12, .col-md-9, .col-md-8, .col-md-6) > .widget-calendar {...}
/* All widgets on the first row: */
#campaign-dashboard > .dashboard-widgets > .row:first-child > .widget {...}
/* Any list widget on the last row: */
#campaign-dashboard > .dashboard-widgets > .row:last-child .widget-recent {...}
/* All widgets with our custom class except a specific one, once we have determined its ID: */
#campaign-dashboard > .dashboard-widgets .custom-class-name:not(#dashboard-widget-1234) {...}
/* Note that custom classes are applied to div.panel,
NOT to the top-level div.widget, so it may not be
suitable for resizing and other effects targeting
the outermost container. */
All widgets except text headers and map previews use a similar content structure: a div.col-md-
specifying the width, a div.widget
with classes specific to the type of widget, a div.panel
with the widget’s unique ID and classes specific to its type, and typically a combination of div.panel-heading
and div.panel-body
. The heading and body are structured according to a few different models which are detailed below.
The simplest heading is used on list widgets such as Unmentioned entities and Recently modified entities, as well as entity previews in their most basic form. It simply consists of an h3.panel-title
and, on entity previews, a link to the entity and an optional icon for private entities and dead characters in an i.pull-right
that is right-floated.
<!-- List widget - no link -->
<div class="panel-heading">
<h3 class="panel-title"> Entity or widget name
<span class="pull-right">
<span class="label label-tag-bubble color-tag label-default" title=""></span>
</span>
</h3>
</div>
<!-- Entity preview - link & dead character icon -->
<div class="panel-heading">
<h3 class="panel-title">
<a href="https://kanka.io/en-US/campaign/...">
<i class="ra ra-skull pull-right mr-2" title="Dead"></i> Entity name </a>
</h3>
</div>
On entity previews that are set to use the entity’s image or header image (and where such image exists), the top structure is slightly different. The addition of .panel-heading-entity
is accompanied by the background image in an inline style declaration and applies additional formatting to the heading’s child elements (most notably padding on the title anchor).
<div class="panel-heading panel-heading-entity" style="background-image: url('https://images.kanka.io/user/....jpg')">
<h3 class="panel-title">
<a href="https://kanka.io/en-US/campaign/..."> Entity name </a>
</h3>
</div>
On map entity previews, div.panel-title
is completely absent, so workarounds are required for users wanting to add a title to those widgets. See example below.
When an entity preview is set to display its full entry in the Setup tab, .panel-body
is at its most simple and contains only the entity’s content:
<div class="panel-body">
<div class="entity-content">
...
</div>
</div>
By default, only part of the entry is visible, up to 200 pixels in height, with a toggle at the bottom of the widget to expand it and show the full entry. .panel-body
’s first child div gains a specific ID as well as a pair of classes that indicate its expanded/collapsed state: .pinned-entity.preview
(closed) and .pinned-entity.full
(expanded, which removes the max-height
from .panel-body
). This div contains div.entity-content
and is followed by the anchor that toggles expansion: a.preview-switch
.
<div class="panel-body">
<div class="pinned-entity preview" data-toggle="preview" id="widget-preview-body-61679">
<div class="entity-content">
...
</div>
</div>
<a href="#" class="preview-switch" id="widget-preview-switch-61679" data-widget="61679">
<i class="fa-solid fa-chevron-down"></i>
</a>
</div>
On entities that are short enough to need less than 200px height, .panel-body
’s first child keeps its ID but has no class, and the toggle gains the .hidden
class which removes it from the layout (display: none;
):
<div class="panel-body">
<div class="" data-toggle="preview" id="widget-preview-body-238277">
<div class="entity-content">
...
</div>
</div>
<a href="#" class="preview-switch hidden" id="widget-preview-switch-238277" data-widget="238277">
<i class="fa-solid fa-chevron-down"></i>
</a>
</div>
This could for instance allow you to force a minimum height on short entities: div[data-toggle="preview"]:not(.pinned-entity) { min-height: ... }
.
The Advanced tab of widget settings allows you to include pinned relations or attributes in the preview, and Family or Organization members. Doing so adds corresponding divs under the entity content, which allows you to style and position these blocks creatively independently from the rest of the content (see below for an example):
<div class="entity-content">
...
</div>
/* Optional Relations block */
<div class="widget-advanced-relations">
<dl class="dl-horizontal">
<li class="list-group-item pinned-relation" data-target="949589" data-relation="Relation target" data-visibility="">
<strong> Relation name </strong>
<span class="pull-right">
<a class="name" data-toggle="tooltip-ajax" data-id="949589" data-url="https://kanka.io/en-US/campaign/.../tooltip" href="https://kanka.io/en-US/campaign/..." data-original-title="" title="">Relation target</a>
</span>
<br class="clear">
</li>
...
</dl>
</div>
/* Optional Attributes block */
<div class="widget-advanced-attributes">
<dl class="dl-horizontal">
<li class="list-group-item pinned-attribute " data-attribute="Pinned attribute name" data-target="3173471" data-private="true">
<strong title="Pinned">Pinned attribute name</strong>
<span class="pull-right">Pinned attribute value</span>
<br class="clear">
</li>
...
</dl>
</div>
/* Optional group members block */
<div class="widget-advanced-members">
<dl class="dl-horizontal">
<dt>Member role</dt>
<dd><a class="name" data-toggle="tooltip-ajax" data-id="530085" data-url="https://kanka.io/en-US/campaign/.../tooltip" href="https://kanka.io/en-US/campaign/..." data-original-title="" title="">Member name</a></dd>
...
</dl>
</div>
dl-horizontal
may also show up in other places in certain cases, for example to show the "instigator" of Quest entities before the entry.
When entity lists show a single entity preview, the panel gains an additional class: .panel-body.widget-recent-body
. This panel wraps the preview in a div.entity
, which starts with some information like the entity’s image and name (as a link) and how long ago it was edited. That subheader is followed by div.preview
, which contains the usual entity content and is followed by the expand/collapse toggle if needed.
<div class="panel-body widget-recent-body">
<div class="entity">
<span class="pull-right elapsed" title="2023-01-09 18:19:18">
<i class="far fa-clock" aria-hidden="true"></i> 1 week ago </span>
<a class="entity-image" style="background-image: url('https://kanka.io/images/defaults/journals_thumb.jpg');" title="Entity name" href="https://kanka.io/en-US/campaign/..."></a>
<a class="name" data-toggle="tooltip-ajax" data-id="3760704" data-url="https://kanka.io/en-US/campaign/.../tooltip" href="https://kanka.io/en-US/campaign/..." data-original-title="" title="">Entity name</a>
<div class="pinned-entity preview" data-toggle="preview" id="widget-preview-body-52696">
<div class="entity-content">
...
</div>
</div>
<a href="#" class="preview-switch" id="widget-preview-switch-52696" data-widget="52696">
<i class="fa-solid fa-chevron-down"></i>
</a>
</div>
</div>
The standard list widget applies the .widget-recent-list
class to the panel body and contains up to ten rows of results (div.flex
), each containing elements for the entity’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.
<div class="panel-body widget-recent-list">
<div class="flex">
<a class="entity-image" style="background-image: url('https://kanka.io/images/defaults/notes_thumb.jpg');" title="Entity name" href="https://kanka.io/en-US/campaign/...">
</a>
<a class="name" data-toggle="tooltip-ajax" data-id="949589" data-url="https://kanka.io/en-US/campaign/.../tooltip" href="https://kanka.io/en-US/campaign/..." data-original-title="" title="">Entity name</a>
<i class="fa-solid fa-lock" title="This entity is private and only visible to members of the campaign's Admin role."></i>
<div class="blame"> Salvatos<br class="hidden-xs">
<span class="elapsed" title="2023-01-17 04:19:41"> 1 day ago </span>
</div>
</div>
...
<div class="text-center">
<a href="#" class="text-center widget-recent-more" data-url="https://kanka.io/en-US/campaign/.../dashboard/widgets/recent/...?page=2">
<span>Next</span>
<i class="fa-solid fa-spinner fa-spin spinner" style="display: none;"></i>
</a>
</div>
</div>
Note that when you load additional results, they are added after the current "Next" link and a new div.text-center
is appended at the end. However, the old "Next" button remains in place 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 link. This is easily avoidable by targeting .widget-recent-list > .text-center:last-child {...}
specifically – though you could do the opposite if you wanted to add a visible separation between groups of 10 results: .widget-recent-list > .text-center:not(:last-child) {...}
.
For calendars, .panel-body
has a few main parts. The first div.widget-loading
holds a simple spinning animation shown during date changes and hidden the rest of the time. div.widget-body
contains the current date and switchers, followed by a div.row
that contains two sections for recent and upcoming events in a responsive one- or two-column layout.
<div class="panel-body" id="widget-body-238212">
/* Loading spinner */
<div class="widget-loading text-center" style="display: none;">
<i class="fa-solid fa-spin fa-spinner fa-4x"></i>
</div>
/* Actual content */
<div class="widget-body" style="">
/* Current date block */
<div class="current-date" id="widget-date-238212">
<a href="#" class="widget-calendar-switch" data-url="https://kanka.io/en-US/campaign/.../dashboard/widgets/calendar/.../sub" data-widget="238212">
<i class="fa-solid fa-chevron-circle-left" data-toggle="tooltip" title="" data-original-title="Change date to previous day"></i>
</a>
<span>Current campaign date </span>
<a href="#" class="widget-calendar-switch" data-url="https://kanka.io/en-US/campaign/.../dashboard/widgets/calendar/.../add" data-widget="238212">
<i class="fa-solid fa-chevron-circle-right" data-toggle="tooltip" title="" data-original-title="Change date to next day"></i>
</a>
</div>
/* Reminders block */
<div class="row">
/* Recent reminders */
<div class="col-md-12 col-lg-6">
<h4> Previous <a href="//docs.kanka.io/en/latest/guides/dashboard.html#known-limitations" target="_blank">
<i class="fa-solid fa-question-circle" data-toggle="tooltip" title="" data-original-title="Why are these reminders being shown?"></i>
</a>
</h4>
<ul class="list-unstyled">
<li data-ago="1">
<div class="pull-right">
<i class="fa-solid fa-calendar" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="Reminder date "></i>
</div>
<a href="https://kanka.io/en-US/campaign/...">Entity name</a>
</li>
/* More reminders... */
</ul>
</div>
/* Upcoming reminders */
<div class="col-lg-6 col-md-12">
<h4> Upcoming <a href="//docs.kanka.io/en/latest/guides/dashboard.html#known-limitations" target="_blank">
<i class="fa-solid fa-question-circle" data-toggle="tooltip" title="" data-original-title="Why are these reminders being shown?"></i>
</a>
</h4>
<ul class="list-unstyled">
<li data-in="19">
<div class="pull-right">
<i class="fa-solid fa-calendar" title="" data-toggle="tooltip" data-placement="bottom" data-original-title="Reminder date "></i>
</div>
<a href="https://kanka.io/en-US/campaign/..." data-toggle="tooltip" data-original-title="" title="">Entity name</a>
</li>
/* More reminders... */
</ul>
</div>
</div>
</div>
</div>
Map previews are too complex to be explored in detail here, but .panel-body
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.
<div class="panel-body">
<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; outline: none;" tabindex="0">
<a href="https://kanka.io/en-US/campaign/.../explore" target="_blank" class="btn btn-primary btn-xs btn-map-explore"><i class="fa-solid fa-map"></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>
Text headers are simply a heading (your choice from h1 to h6), optionally wrapped in an anchor if a target entity 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.
<div class="widget widget-header">
<a href="https://kanka.io/en-US/campaign/...">
<h3 class="widget-header-text text-center" id="dashboard-widget-205449"> Header text </h3>
</a>
</div>
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 entity preview and hide its header, relying solely on the entry’s content for your layout.
Here is a simple bit of CSS I use on my session log (Entity list widget, in preview mode showing the latest Journal) to turn pinned relations into a sidebar, similar to pins on the entity 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.
/* 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 and
add margin between the two columns */
#widget-preview-body-52696 .entity-content {
text-align: justify;
margin-right: 10px;
}
/* Make a cleaner separation between relation names and items */
#widget-preview-body-52696 .pinned-relation strong {
display: block;
}
#widget-preview-body-52696 .pinned-relation .pull-right {
text-align: right;
}
For those wanting symmetrical widgets, the absence of a header on map previews can be pretty annoying. Although we can’t create a link to the entity, we can at least fake a title using a pseudo-element. Here is an example for an unmodified campaign using the Default style:
/* Put the name in a ::before pseudo-element,
reusing styles from .panel-heading and .panel-title */
#dashboard-widget-44291 .panel-body::before {
content: "Arcadie";
display: block;
padding: 10px 15px;
font-size: 12pt;
border-bottom: 1px solid #d3e0e9b5;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
And one tailored to my Default Widget Banner theme:
/* Mimic Default Banner on a map widget */
#dashboard-widget-44291 .panel-body::before {
content: "Arcadie";
padding: 30px 20px;
display: inline-block;
width: 100%;
font-size: 18px;
color: #fff;
text-shadow: rgba(0,0,0,.9) 0 1px 4px;
background-image: var(--default-banner-100p);
background-size: cover;
background-repeat: no-repeat;
background-position: 50% 10%;
border-bottom: 1px solid #d3e0e9b5;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
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:
.map-dashboard {
height: 400px !important;
}
As of Kanka 0.48, you can create additional dashboards that can be targeted via a class on the body tag, .dashboard-(id)
. Note that the default dashboard is not identified by a class as of v1.39, 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 and can be styled in a specific way for each dashboard.
As mentioned earlier, custom dashboards let you choose whether or not to display the Campaign Header. Repeating the Campaign Header there may seem a little boring or unnecessary, but you may still want to include it for the dashboard switcher and banner image, for instance. 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 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:
/* 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;
}
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.
If this guide helped you, a tip goes a long way to keep me making more of this kind of content :) I am also sometimes available for commissions to help directly with your templates or CSS. You can find me on Ko-fi: