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.
Layout
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 all dashboard widgets and a final div for the Dashboard Settings button. Here is a (simplified) general overview of the page’s HTML structure:
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.
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.
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). 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() pseudo-class, for example div.widget:has(.blue-widget) {...}
.
Widget size and layout
On the dashboard, widgets are laid out using a CSS 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.
For example, a 100%-width widget occupies all 12 columns via the class div.md:col-span-12
, while a 33%-width widget occupies 4 columns via div.md:col-span-4
. Therefore, you can target all widgets of a given format with a rule such as .md\:col-span-12 > .widget {...}
.
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 😤
The following table shows each available widget size by name, percentage and class.
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.
Entity preview
div.widget.widget-preview
>
div.widget-preview
Entity preview (map)
div.widget.widget-preview
>
div.widget-preview
>
div.widget-map
Random entity preview
div.widget.widget-random
>
div.widget-random
Calendar
div.widget.widget-calendar
>
div.widget-calendar
Entity list (such as unmentioned or recently modified)
div.widget.widget-recent
>
div.widget-list
Entity list (single entity preview)
div.widget.widget-recent
>
div.widget-preview
Text header*
div.widget.widget-header
*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 Header widgets’ outer container, while the second targets the header inside most widgets.
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:
Widget structure
All widgets except text headers, map previews and attribute 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 entities and Recently modified entities. It simply consists of an h4.text-lg
with two spans:
However, when using a Recently modified list for a single entity type, the header will have a link instead of plain text, which may lead to inconsistent styling between lists if not accounted for:
Entity link header
Also quite simple, entity previews have a div.header
with a link. Private entities and dead characters have an extra span to denote their status with an icon:
Entity image header
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 image is shown first separately, and the title underneath; both of them link to the entity:
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 entity previews, there is no header at all, but it is possible to fake one with CSS (example below). There is however an extra layer before the actual content, with div.widget-map
identifying the entity as a map.
Entity preview body (full)
When an entity preview is set to display its full entry in the Setup tab, .widget-body
is at its most simple and contains only the entity’s content:
Entity preview body (default)
By default, only part of the entry 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 entry. .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
.
Entity preview body (with pinned relations, attributes or group members)
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):
List body (such as Recently modified and Unmentioned entities)
List widgets, unless set to show only the first result’s entity 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 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.
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:
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:
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:
.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:
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.
Text header
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.
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.
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.
Attributes iframe
If you choose the Attributes option in the Display dropdown of an entity preview, the relevant part of the entity’s attribute page will be injected as an iframe element. This can display either a raw list of attributes as dl/dt elements, or a fully customized Marketplace character sheet if one is set for that entity.
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 can come in handy to help you write width-based rules rather than parent-based ones, and thus make sure your attributes 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 (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.
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 entity, 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:
And one with a background image (legibility could be better and a border might be nice, but this covers the basics):
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:
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:
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.
Last updated