The Ability section represents the core of this sheet so we’ll spend a full post on this. There’s a lot happening behind the scenes (some of that crosses over into the next post, on Advancement), but we’ll try to keep it simple.
Abilities
There are only 6 abilities, or skills, though each is very broad: Audacity, Daring, Ferocity, Flair, Shenanigans, and Tinkering. Each Ability has a rank, typically from Poor to Spectacular. There are other ranks, but they aren’t usually used by PCs except through modifiers.
The full list or ranks is Terrible, Poor, Fair, Good, Great, Superb, Spectacular, Legendary, Mythic, and Perfect, though most of those are only included for completeness and aren’t really used. While they are identified with an adjective, which is great for referring to them in common language, they also have a numeric score ranging from 1-10 which is handy for skill rolls.
You need each skill listed, a drop down where the rank can be selected, and an input to hold the numeric value. That is calculated by a sheet worker. Finally, the skill name should be a button to launch a skill roll.
Our first pass at generating these skills looks like this:
<h2>Abilities</h2>
<div class="ability">
<button type="roll" name="roll_Audacity" class="nod20 Audacity tokenaction"
value="&{template:ladder} {{header=1}} {{who=@{character_name} }} {{skill=Audacity }} {{die1=[[1d10]] }} {{die2=[[1d10]] }} {{score=[[@{audacity_score} ]] }}">
<span>Audacity</span>
</button>
<select name="attr_audacity">
<option >Terrible</option>
<option selected>Poor</option>
<option >Fair</option>
<option >Good</option>
<option >Great</option>
<option >Superb</option>
<option >Spectacular</option>
<option >Legendary</option>
<option >Mythical</option>
<option >Perfect</option>
</select>
<input type="number" name="attr_audacity_score" value="">
</div>
Code language: HTML, XML (xml)
That generates the image below. It is functional, but needs some styling.

There are a couple of things to note here:
First, we start with a roll button, followed by hidden inputs and action buttons. These are a solution created by Scott M to work around problems with Roll20. Action Buttons are needed to launch custom roll parsing, but cannot be added to the macrobar, so we need to use a roll button and have that trigger the hidden action button.
In sheet worker code, the hidden input is assigned code, which is then assigned to the roll button. That includes the character name, which is needed to trigger the proper action button, and accounts for if the player manually changes their character sheet name.
We also need another sheet worker, to properly fill the score input. Finally the styling of the objects in the Abilities box is a bit of a mess, we want to make that look a bit prettier. We’ll do that first.
CSS For The Abilities
With the first pass of trial-and-error CSS changes, we change the above image to this:

That looks much better. The columns are lined up nicer and it takes up less space. Let’s look at how that is created.

We start out with skill buttons like this. We want to remove that die icon at the start and change the text size. Changing the icon is tricky, because of the way it’s been added to the button. Each element has a ::before
property (and an ::after
property). If you add to that before
property, it will show up regardless of what you change the text to. So we need to remove that with:
button.nod20::before {
content: "" !important;
}
Code language: CSS (css)
I created a nod20
class to add to the button, so buttons without it would be unchanged. I might want to use them, after all. (I probably won’t, but the option is there).
To change the text-size and orientation was a lot easier. I could add a few extra things too.
.charsheet button[type="roll"].nod20 {
margin: 0;
padding: 0;
padding-left: 2px;
border: 1pt solid black; /* gray; */
border-radius: 4px;
font-size: 13px;
text-align: left;
width: 100%;
}
Code language: CSS (css)
I have added line breaks to make it easier to tell the sections I was thinking of. I needed to add the .charsheet
at the start to override some spacing issues. You can always add .ui-dialog .charsheet
to the start of your declarations to make them stickier. There’s no downside to doing this – you could add them to all your declarations if you wanted. I like to save myself some typing and only add them when explicitly needed.
Removing The Spinner
The input at the far right has a spinner
– an up-down control that changes number values one by one. Since this input is not meant to be changed by users, only when the rank changes, we dpon’t need that. Removing it is a little tricky. I add the no-spinner
class and add this CSS:
input.no-spinner::-webkit-outer-spin-button,
input.no-spinner::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
input.no-spinner[type=number] {
-moz-appearance: textfield;
}
input.no-spinner {
text-align: center;
}
Code language: CSS (css)
There are many types of browser, and your code has to accommodate them all. When you see the word webkit
, that tells you this code is used by chrome and the many browsers based on it. Likewise, moz
is short for mozilla, and tells you this is for firefox. This code is pretty universal, and can be easily copied to any sheets where you need to remove spinners from number inputs. (I nearly always remove them, so have gotten a lot of use out of this snippet).
I made another tweak to the HMTL code for the input for later.
<input type="number" name="attr_audacity_score" class="no-spinner" value="2" readonly>
Code language: HTML, XML (xml)
The value means the default value is what it should be, and the readonly
tag at the end means that the value isn’t changed except through sheet workers.
Arranging into Columns – CSS Grid Again
We still need to arrange the code into neat columns – that’s where CSS Grid comes in handy again. You can next grids inside other grids. We have used Grid to organize the sheet, and now we’ll use it again to create these columns. This is extremely easy:
.ability {
display: grid;
grid-template-columns: 7em 8em 2em;
column-gap: 5px;
}
Code language: CSS (css)
The CSS Reset
Roll20 assigns some inconvenient styling to some elements (selects and number inputs are notorious for this), so its a good idea to create something called a CSS Reset: this is section at the start of your CSS where you include any standard styling code you use. I put the no-spinner
and nod20
code here, but this is also handy:
.ui-dialog .charsheet select,
.ui-dialog .charsheet input[type=number] {
width: 100%;
}
.ui-dialog .charsheet select {
margin-bottom: 1px;
}
Code language: CSS (css)
This makes number inputs
and selects
more likely to fit your styling. You can override this if desired, but it works for this section of the sheet.
Final CSS Note
More styling for this section would be nice (and will be applied before the final version), but that will do for now.
By the way, the CSS for my sheets always looks like a total mess. It doesn’t matter how organized my HTML file looks, the CSS file always quickly becomes a total mess. I start out with good intentions, but it doesn’t last. Don’t worry if this happens to you – the organization rarely matters.
The Ability Sheet Workers
The rolls are their own topic, so we’ll cover them in the next post. That means that we need one simple sheet worker, to calculate a numerical index or each skill rank (Terrible to Perfect, or most likely Poor to Spectacular).
We’ll first create variables to hold the skills and rank names, since these will be used a lot. Then we’ll create a loop to cycle through eah skill, and enter its rank index so that people who prefer numbers over adjectives can also see it in the sheet. Here’s the script code. There’s nothing much to it, but there are some things to explain.
const skills = [ "audacity", "daring", "ferocity", "flair", "shenanigans", "tinkering", ];
const ranks = [ "terrible", "poor", "fair", "good", "great", "superb", "spectacular", "legendary", "mythical", "perfect", ];
skills.forEach(skill => (
on(`change:${skill} sheet:opened`, () => {
getAttrs([skill], v => {
const rank = v[skill];
const score = 1 + (ranks.indexOf(rank.toLowerCase()) || 0);
setAttrs({
[`${skill}_score`]: score
});
});
})
));
Code language: JavaScript (javascript)
First, all the rank and skill names are in lower case – they could be in title case (start with capital letters), but whichever you use, those code needs functions to handle it, so I wet the way I’m most comfortable.
Secondly, the forEach
loop. A long time ago, I created what I called back then, Universal Sheet Workers. This is a fairly simple and standard JavaScript construction, but I hadn’t seen them used on roll20 character sheets before then. Now I have seen them used many times – which gives me warm and fuzzy feelings.
Feel free to examine that code, and ask questions about how it works. And that’s that – skills are done. On to Skill Rolls (and the rollTemplate and custom roll parsing needed)