Documentation

Theming Janela

styling · theming · css · host-integration

Janela publishes the hooks; a theme supplies the taste. This page is the contract: the class names and custom properties the engine renders, which your own stylesheet or somebody else’s theme may target, and which will not change without an entry in UPGRADING.md (ADR 036).

Two stylesheets ship in the gem.

<%= stylesheet_link_tag "janela" %>   <%# the hooks, and enough style to be legible %>
<%= stylesheet_link_tag "vitral" %>   <%# optional: one theme, stained glass %>

janela.css is not a look. It is the grid, the pane’s own markup and enough base styling that a table does not run its label into its number. Everything decorative is a theme’s, and vitral is one theme rather than the theme.

The contract

Custom properties

Setting any of these moves everything that depends on it.

Property Default What it does
--janela-space 0.25rem The base unit of the whole spacing scale. Every gap and padding is a multiple of it.
--janela-line rgba(128, 128, 128, 0.3) Rules between rows, borders on cards and fields.
--janela-accent rgb(54, 162, 235) A selected value, a hovered card, a line chart (#62), and the first colour of the palette below.
--janela-series-1 to --janela-series-8 the accent, then #eb6834, #1baf7a, #eda100, #e87ba4, #008300, #4a3aa7, #e34948 The colour a bar, a doughnut slice or a pie slice is drawn in, by its position in the pane, first to eighth (ADR 046).
--janela-series-other #8c8c8c Every position after the eighth. The palette is never cycled, so the ninth value is not drawn like the first.
:root {
  --janela-space: 0.3rem;
  --janela-accent: #7c3aed;
}

The grid

A frame’s stored integers choose these. Nothing an analyst types reaches CSS as a length: the number selects a rule that is already written (ADR 016).

Class Range
janela-frame the grid container itself
janela-cols-N 1 to 12
janela-gap-N 0 to 8, multiplied by --janela-space
janela-span-N 1 to 12, on a pane

Below 40rem the grid collapses to one column. That is in the stylesheet rather than in the data, because a dashboard nobody can read on a phone is not a choice worth offering.

The pane

What janela_pane and janela_frame put in your own pages. These are the names a theme spends most of its time on.

Class On Rendered when
janela-pane <table>, <p>, <figure> or <div> every pane, whatever the renderer
janela-value <p> a single value pane
janela-fact <th>, <td> a table’s companion column that is a dimension’s shared fact rather than a number, so it is set left. A table with companions also has a <thead>; without them it has none (ADR 051)
janela-value-label <span> its caption
janela-value-number <strong> the number itself
janela-prominence-1 to janela-prominence-3 <p> on a single value that declares how prominent it is: the number is 1.25rem, 2rem or 3.5rem. Step 2 is what a value is without the class (ADR 050)
janela-chart <canvas> a bar or line pane, inside its <figure>
janela-chart-title <figcaption> its caption (ADR 042)
janela-chart-box <div> around the canvas of a bar or line pane that has a height; a pane without one has no box
janela-h-1 to janela-h-5 <div> the box’s height, in steps of the spacing unit: 24, 40, 56, 80 and 112 times --janela-space, so 96, 160, 224, 320 and 448px by default (ADR 047)
janela-ring <figure> a doughnut or pie pane (ADR 046)
janela-ring-svg <svg> its picture; data-hole is true for a doughnut
janela-ring-slice <path> one slice; janela-dim is added to those not selected while something is
janela-legend <table> its legend, one row of swatch, button and value per slice
janela-swatch <span> the colour beside a legend label
janela-empty <p> a pane whose query returned nothing
janela-muted <p> the note under a ring that could not be drawn and became a table (ADR 046)
janela-error <p> a pane that could not be read
janela-content <div> a stored pane holding words or a host partial rather than a query (ADR 039)
janela-content-heading <h2> a text pane’s heading

The dark values that passed the palette check on a dark surface are #3987e5, #d95926, #199e70, #c98500, #d55181, #008300, #9085e9 and #e66767. janela.css ships no dark scheme (ADR 023), so a dark theme sets the eight properties to them.

A table pane renders a <caption> and a chart pane a <figcaption>, each its own accessible name; the chart’s canvas points to its figcaption with aria-labelledby rather than repeating the string in aria-label, so the two cannot drift apart (ADR 042).

Hiding a heading you already wrote

Put janela-own-headings on any ancestor and a table’s caption, a single value’s label and a chart’s title are hidden from sight while staying in the accessibility tree.

<div class="janela-own-headings">
  <h3>Revenue by status</h3>
  <%= janela_pane Order, :revenue, by: :status %>
</div>

Use it when your own markup already says what the pane is, so the text is not on screen twice. It hides rather than removes on purpose: a pane with no visible caption still needs its accessible name, so a screen reader lands on a grid of numbers with nothing to say what they measure. display: none would do that, which is why this rule ships here rather than being left for each host to write.

What is not the contract

janela.css also styles Janela’s own pages, the frame index and the editing forms: janela-page, janela-card, janela-button, janela-form, janela-field, janela-list, janela-crumb, janela-flash and the rest. They are plain class names that the engine’s own pages use, not scoped to janela-page, so they affect your pages only if your own markup uses the same names. Avoid them.

Those names may change in any release. They are the engine’s own chrome rather than an interface. Restyle them if you want Janela’s pages to match your application, and expect to revisit it after an upgrade; or point Janela.theme at a stylesheet of your own, below.

Writing a theme

A theme is any stylesheet, named once:

# config/initializers/janela.rb
Janela.theme = "midnight"

The name is resolved against your own asset paths, so midnight.css in your application works exactly as the gem’s own vitral does. A name that your asset pipeline cannot find raises when the page renders, rather than quietly rendering an unthemed one.

That setting links the theme into Janela’s own pages. Your pages load whatever your layout says, so if you want the same look around a pane you have embedded yourself, link the stylesheet there too. The asymmetry is deliberate: Janela is an isolated engine and does not write your layout (ADR 011).

A theme targets the contract above and nothing else. If it cannot be written that way, the contract is missing something, which is worth an issue rather than a workaround.

Vitral

The theme the gem ships. A vitral is a stained glass window: each pane holds one of five colours, dark leading runs between them, and the light comes from behind.

<%= stylesheet_link_tag "vitral" %>
<body class="vitral">

Nothing is repainted until that class is present, so linking the stylesheet can never be the thing that broke a page.

Its own public classes, so the page around a dashboard can be made of the same window: vitral-pane, vitral-panes, vitral-button, vitral-button-primary, vitral-lattice.

Retheme it from its custom properties rather than by forking it. The light is --vitral-light-cobalt, -teal, -amber, -rose, -violet; the glass is --vitral-pane-1 through -5; the leading is --vitral-came; and --vitral-ink, --vitral-muted, --vitral-ground, --vitral-radius, --vitral-shadow and --vitral-blur do what they say.

:root {
  --vitral-pane-1: rgba(120, 60, 200, 0.3);
  --vitral-came: #1b1b1b;
}

It works without JavaScript. Janela’s own pages load none (ADR 011), so the stained glass is CSS and the lattice that leans toward the pointer is a separate optional controller. Reduced motion, reduced transparency and increased contrast each fall back to a still, solid window, and so does a browser without backdrop-filter.

test/dummy has a live page at /vitral showing all of it against real panes.

All documentation This page in the repository