Styling Harlowe
- The style of your story should compliment its tone and setting.
- Your browser’s inspector is the best way to correctly identify all Harlowe elements for styling.
Note: Harlowe features a DOM View when you run Test from Twine’s build options. Not all elements are identified this way though, but the majority will be. However, it can be really disruptive to the way your story is presented (hard to navigate your story) and it does not recognize any deliberate passage HTML.
Using The Inspector :
When running your story, in the browser, pressing F12 on the keyboard will launch the inspector. Right-clicking on a page element and selecting Inspect will open the inspector and highlight that element’s specific HTML code.
It may look daunting at first, but learning to navigate the inspector will give you all the information you need to create your own styles and override any that Harlowe interferes with.
》Tip: It’s best to change the inspector to use the side of the browser window to get the most vertical scrolling space available. You’ll be scrolling and expanding HTML elements quite a bit as you dive into the HTML code that Harlowe generates.
You’ll notice that Harlowe has a set structure with it’s own custom HTML tags:
<html>
<head>
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title>Harlowe Example</title>
<style title=”Twine CSS”> … </style> <== harlowe’s style sheet
<style data-title="Story stylesheet '1'"> … </style> <== story style sheet
</head>
<body>
<tw-storydata name="Harlowe Story Title" startnode="1" creator="Twine" creator-version="2.5.1" format="Harlowe" format-version="3.3.3" ifid="..." options="" tags="" zoom="1" hidden="">
<style role="stylesheet" id="twine-user-stylesheet" type="text/twine-css"> … </style> <== story style sheet (duplicated above in the <head> tag)
<script role="script" id="twine-user-script" type="text/twine-javascript"> … </script> <== story javascript
<tw-passagedata pid="1" name="Start" tags="" position="200,100" size="100,100"> … </tw-passagedata> <== twine passage content (including editor passage map locations)
</tw-storydata>
<script title="Twine engine code" data-main="harlowe"> … </script> <== harlowe api javascript
<tw-story tags=""> <== (!) gets removed and then appended to the bottom of the body tag as new passages are visited
<tw-passage tags="">
<tw-sidebar> … </tw-sidebar>
<tw-include type="startup" name="Startup"> … </tw-include> <== startup passage (only exists for the first passage shown – disappears afterwards)
<tw-include type="header" name="Header"> … </tw-include> <== header passage
… <== (!) current passage html
<tw-include type="footer" name="Footer"> … </tw-include> <== footer passage
</tw-passage>
</tw-story>
</body>
</html>
…and Harlowe will consistently nest it’s elements as such. You’ll always find the currently displayed passage code between the <tw-include>
tags for the header and footer (if they exist).
So the base elements that affect how our story appears are:
<html>
<body>
<tw-story>
<tw-passage>
It’s important to understand that certain styles are inherited by the parent element so some styles in the <body>
affect <tw-story>
, <tw-passage>
and so on.
》Tip: It’s common practice to hide the <tw-sidebar>
with display: none;
if you don’t want the user to backtrack and undo their choices.
Harlowe markup elements also have the ability to be styled:
-
#
, ##
, ###
… ######
= <h1>
, <h2>
, <h3>
… <h6>
-
//
= <b>
-
''
= <i>
-
~~
= <s>
-
*
= <em>
-
**
= <strong>
-
^^
= <sup>
Another important category of elements are links. Links are generated as HTML in a variety of ways so it’s important to identify the different kinds of links in your story:
-
[[x->y]]
, [[y<-x]]
, [[y]]
= <tw-expression> + <tw-link>
-
(link:)[]
= <tw-hook> + <tw-link>
-
(click:)[]
= <tw-enchantment class="link enchantment-link">
Targeting Harlowe Elements for Styling :
In CSS, defining an element for styling is done through selectors. Selectors are essentially a simple language of their own. Not only can you target specific elements, but you can also apply conditions as to when they get styled. It’s incredibly useful to know how to target nested elements. Some of the basics are as follows:
:root { … } <== root variables that are used throughout the style sheet
tw-story { … } <== <tw-story> tag
tw-enchantment.link { … } <== <tw-enchantment> tag with a class="link"
h1, h2, h3, h4, h5, h6 { … } <== all 6 html headers tags in one style
tw-dialog-links tw-link { … } <== <tw-link> tag within the <tw-dialog-links> parent tag
tw-link:hover, tw-enchantment.link:hover { … } <== while the cursor is over the <tw-link> tag or the <tw-enchantment class="link"> tag
…for an in depth list of all CSS selector logic, check out W3Schools’ Selector Reference page.
》 Tip: Targetting Harlowe specific elements requires using the browser’s inspector to properly identify the correct CSS selectors to use. Not only do you see the exact HTML element, but you see its parent elements too. Sometimes, it’s the parent element that contains the unique pattern that you want to use in your selector to style a child element.
Harlowe’s Own Styles :
Your Twine story is usually a self-contained HTML file, which means that all Harlowe CSS is located in the file. I copied the Harlowe CSS and used Notepad++ to find and replace certain patterns to restructure it all to a more readable format. (Otherwise, it’s one very long string of text.)
Here is Harlowe 3.3.3’s CSS code for standard story elements (I’ve removed the debugging and transition styles to avoid displaying too much information):
tw-dialog {
z-index: 999997;
border: #fff solid 2px;
padding: 2em;
color: #fff;
background-color: #000;
display: block
}
@media(min-width: 576px) {
tw-dialog {
max-width: 50vw
}
}
tw-dialog input[type=text] {
font-size: inherit;
width: 100%;
border: solid #fff !important
}
tw-dialog-links {
text-align: right;
display: -ms-flexbox;
display: flex;
-ms-flex-pack: end;
justify-content: flex-end
}
tw-backdrop {
z-index: 999996;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0,0,0,.8);
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-pack: center;
justify-content: center
}
tw-backdrop~tw-backdrop {
display: none
}
tw-link,.enchantment-link {
cursor: pointer;
color: #4169e1;
font-weight: bold;
text-decoration: none;
transition: color .2s ease-in-out
}
tw-passage [style^=color] tw-link: not(: hover),tw-passage [style*=" color"] tw-link: not(: hover),tw-passage [style^=color][hover=true] tw-link: hover,tw-passage [style*=" color"][hover=true] tw-link: hover,tw-passage [style^=color] .enchantment-link: not(: hover),tw-passage [style*=" color"] .enchantment-link: not(: hover),tw-passage [style^=color][hover=true] .enchantment-link: hover,tw-passage [style*=" color"][hover=true] .enchantment-link: hover {
color: inherit
}
tw-link: hover,.enchantment-link: hover {
color: #00bfff
}
tw-link: active,.enchantment-link: active {
color: #dd4b39
}
.visited {
color: #6941e1
}
tw-passage [style^=color] .visited: not(: hover),tw-passage [style*=" color"] .visited: not(: hover),tw-passage [style^=color][hover=true] .visited: hover,tw-passage [style*=" color"][hover=true] .visited: hover {
color: inherit
}
.visited: hover {
color: #e3e
}
tw-broken-link {
color: #933;
border-bottom: 2px solid #933;
cursor: not-allowed
}
tw-passage [style^=color] tw-broken-link: not(: hover),tw-passage [style*=" color"] tw-broken-link: not(: hover),tw-passage [style^=color][hover=true] tw-broken-link: hover,tw-passage [style*=" color"][hover=true] tw-broken-link: hover {
color: inherit
}
tw-link.enchantment-mouseover,.link.enchantment-mouseover,tw-expression.enchantment-mouseover>tw-link {
color: inherit;
font-weight: inherit;
transition: none;
cursor: inherit;
border-bottom: 2px dashed #999
}
tw-link.enchantment-mouseover: hover,tw-link.enchantment-mouseover: active,.link.enchantment-mouseover: hover,.link.enchantment-mouseover: active,tw-expression.enchantment-mouseover>tw-link: hover,tw-expression.enchantment-mouseover>tw-link: active {
color: inherit
}
tw-link.enchantment-mouseover.enchantment-button,.link.enchantment-mouseover.enchantment-button,tw-expression.enchantment-mouseover>tw-link.enchantment-button {
border-style: dashed
}
tw-link.enchantment-mouseout,.link.enchantment-mouseout,tw-expression.enchantment-mouseout>tw-link {
color: inherit;
font-weight: inherit;
transition: none;
cursor: inherit;
border: rgba(64,149,191,.6) 1px solid;
border-radius: .2em
}
tw-link.enchantment-mouseout: hover,tw-link.enchantment-mouseout: active,.link.enchantment-mouseout: hover,.link.enchantment-mouseout: active,tw-expression.enchantment-mouseout>tw-link: hover,tw-expression.enchantment-mouseout>tw-link: active {
color: inherit
}
tw-link.enchantment-mouseout: hover,.link.enchantment-mouseout: hover,tw-expression.enchantment-mouseout>tw-link: hover {
background-color: rgba(175,197,207,.75);
border: rgba(0,0,0,0) 1px solid
}
tw-link.enchantment-dblclick,.link.enchantment-dblclick,tw-expression.enchantment-dblclick>tw-link {
color: inherit;
font-weight: inherit;
transition: none;
cursor: inherit;
cursor: pointer;
border: 2px solid #999;
border-radius: 0
}
tw-link.enchantment-dblclick: hover,tw-link.enchantment-dblclick: active,.link.enchantment-dblclick: hover,.link.enchantment-dblclick: active,tw-expression.enchantment-dblclick>tw-link: hover,tw-expression.enchantment-dblclick>tw-link: active {
color: inherit
}
tw-link.enchantment-dblclick: active,.link.enchantment-dblclick: active,tw-expression.enchantment-dblclick>tw-link: active {
background-color: #999
}
tw-link.enchantment-button,.link.enchantment-button,.enchantment-button: not(.link) tw-link,.enchantment-button: not(.link) .link {
border-radius: 16px;
border-style: solid;
border-width: 2px;
text-align: center;
padding: 0px 8px;
display: block
}
.enchantment-button {
display: block
}
.enchantment-clickblock {
cursor: pointer;
width: 100%;
height: 100%;
display: block
}
.enchantment-clickblock>: not(tw-enchantment): : after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
position: absolute;
pointer-events: none;
color: rgba(65,105,225,.5);
transition: color .2s ease-in-out
}
.enchantment-clickblock>: not(tw-enchantment): hover: : after {
color: rgba(0,191,255,.5)
}
.enchantment-clickblock>: not(tw-enchantment): active: : after {
color: rgba(222,78,59,.5)
}
.enchantment-clickblock>: not(tw-enchantment): : after {
box-shadow: inset 0 0 0 .5vmax
}
.enchantment-clickblock>tw-passage: : after,.enchantment-clickblock>tw-sidebar: : after {
box-shadow: 0 0 0 .5vmax
}
.enchantment-mouseoverblock>: not(tw-enchantment): : after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
position: absolute;
pointer-events: none;
border: 2px dashed #999
}
.enchantment-mouseoutblock>: not(tw-enchantment): : after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
position: absolute;
pointer-events: none;
border: rgba(64,149,191,.6) 2px solid
}
.enchantment-mouseoutblock: hover>: not(tw-enchantment): : after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
position: absolute;
pointer-events: none;
background-color: rgba(175,197,207,.75);
border: rgba(0,0,0,0) 2px solid;
border-radius: .2em
}
.enchantment-dblclickblock>: not(tw-enchantment): : after {
content: "";
width: 100%;
height: 100%;
top: 0;
left: 0;
display: block;
box-sizing: border-box;
position: absolute;
pointer-events: none;
cursor: pointer;
border: 2px solid #999
}
tw-dialog-links {
padding-top: 1.5em
}
tw-dialog-links tw-link {
border-radius: 16px;
border-style: solid;
border-width: 2px;
text-align: center;
padding: 0px 8px;
display: block;
display: inline-block
}
html {
margin: 0;
height: 100%;
overflow-x: hidden
}
*,: before,: after {
position: relative;
box-sizing: inherit
}
body {
margin: 0;
height: 100%
}
tw-storydata {
display: none
}
tw-story {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: column;
flex-direction: column;
font: 100% Georgia,serif;
box-sizing: border-box;
width: 100%;
min-height: 100%;
font-size: 1.5em;
line-height: 1.5em;
padding: 5% 5%;
overflow: hidden;
background-color: #000;
color: #fff
}
tw-story [style*=content-box] * {
box-sizing: border-box
}
@media(min-width: 576px) {
tw-story {
padding: 5% 20%
}
}
tw-story tw-consecutive-br {
display: block;
height: 1.6ex;
visibility: hidden
}
tw-story select {
background-color: rgba(0,0,0,0);
font: inherit;
border-style: solid;
padding: 2px
}
tw-story select: not([disabled]) {
color: inherit
}
tw-story textarea {
resize: none;
background-color: rgba(0,0,0,0);
font: inherit;
color: inherit;
border-style: none;
padding: 2px
}
tw-story input[type=text] {
background-color: rgba(0,0,0,0);
font: inherit;
color: inherit;
border-style: none
}
tw-story input[type=checkbox] {
transform: scale(1.5);
margin: 0 .5em .5em .5em;
vertical-align: middle
}
tw-story tw-noscript {
animation: appear .8s
}
tw-passage {
display: block
}
tw-sidebar {
text-align: center;
display: -ms-flexbox;
display: flex;
-ms-flex-pack: justify;
justify-content: space-between
}
@media(min-width: 576px) {
tw-sidebar {
left: -5em;
width: 3em;
position: absolute;
-ms-flex-direction: column;
flex-direction: column
}
tw-enchantment[style*=width]>tw-sidebar {
width: inherit
}
}
tw-icon {
display: inline-block;
margin: .5em 0;
font-size: 66px;
font-family: "Verdana",sans-serif
}
tw-icon[alt] {
opacity: .2;
cursor: pointer
}
tw-icon[alt]: hover {
opacity: .4
}
tw-icon[data-label]: : after {
font-weight: bold;
content: attr(data-label);
font-size: 20px;
bottom: -20px;
left: -50%;
white-space: nowrap
}
tw-meter {
display: block
}
tw-hook: empty,tw-expression: empty {
display: none
}
tw-error {
display: inline-block;
border-radius: .2em;
padding: .2em;
font-size: 1rem;
cursor: help;
white-space: pre-wrap
}
tw-error.error {
background-color: rgba(223,58,190,.6);
color: #fff
}
tw-error.warning {
background-color: rgba(223,140,58,.6);
color: #fff;
display: none
}
.debug-mode tw-error.warning {
display: inline
}
tw-error-explanation {
display: block;
font-size: .8rem;
line-height: 1rem
}
tw-open-button,tw-folddown {
cursor: pointer;
line-height: 0em;
border-radius: 4px;
border: 1px solid rgba(255,255,255,.5);
font-size: .8rem;
margin: 0 .4rem;
padding: 3px;
white-space: pre
}
tw-folddown: : after {
content: "▶"
}
tw-folddown.open: : after {
content: "▼"
}
tw-open-button[replay] {
display: none
}
tw-error tw-open-button,tw-eval-replay tw-open-button {
display: inline !important
}
tw-open-button: : after {
content: attr(label)
}
tw-notifier {
border-radius: .2em;
padding: .2em;
font-size: 1rem;
background-color: rgba(223,182,58,.4);
display: none
}
.debug-mode tw-notifier {
display: inline
}
tw-notifier: : before {
content: attr(message)
}
tw-colour {
border: 1px solid #000;
display: inline-block;
width: 1em;
height: 1em
}
tw-enchantment: empty {
display: none
}
h1 {
font-size: 3em
}
h2 {
font-size: 2.25em
}
h3 {
font-size: 1.75em
}
h1,h2,h3,h4,h5,h6 {
line-height: 1em;
margin: .3em 0 .6em 0
}
pre {
font-size: 1rem;
line-height: initial
}
small {
font-size: 70%
}
big {
font-size: 120%
}
mark {
color: rgba(0,0,0,.6);
background-color: #ff9
}
ins {
color: rgba(0,0,0,.6);
background-color: rgba(255,242,204,.5);
border-radius: .5em;
box-shadow: 0em 0em .2em #ffe699;
text-decoration: none
}
center {
text-align: center;
margin: 0 auto;
width: 60%
}
blink {
text-decoration: none;
animation: fade-in-out 1s steps(1, end) infinite alternate
}
tw-align {
display: block
}
tw-columns {
display: -ms-flexbox;
display: flex;
-ms-flex-direction: row;
flex-direction: row;
-ms-flex-pack: justify;
justify-content: space-between
}
…this Harlowe CSS may prove useful for referencing, but remember that all this information can be obtained through the browser’s inspector. The inspector also illustrates inheritance (parent elements controlling child elements) much more clearly.
》 Tip: You can even inject new (or modify existing) CSS style information in the inspector if you want to test something temporarily without having to edit/recompile your story. Specific style information can also be toggled on and off. This is especially useful to test if Harlowe is interfering with a style you want to control, as you can turn off individual styles until you find the culprit.
Rationale :
- Writing your own CSS is good practice for separating the look of your story from the story logic itself.
- The story’s presentation can then change without having to alter the passage code at all.
- Fortunately, there’s not a lot of Harlowe elements that you need to style in order to customize your story.
- Becoming adept with the inspector allows you to dissect any web page you find on the internet to see how certain styling is achieved.
Food For Thought :
Harlowe allows you to style passage elements with custom macros. This is a logical way to apply CSS to your Harlowe story. However, to use macros effectively throughout your story, they should be assigned to story variables $
and these are saved in game save slots. If your project, gets updated to a new version, previous save game data might have incorrect macro styling data and you’ll need to account for that when loading older save game versions. Another concern is that, with the act of saving all styling macros, save game data may get quite large depending on the complexity of your story. Perhaps in the future, Harlowe will distinguish between global variables (story variables that are ignored when saving) and regular story variables. However, by using CSS directly on HTML elements (like what is being proposed here), this is not a concern.