CSS Wizardry
From Roll20 Wiki
Page Updated: 2021-10-17 |
This is about a Roll20 feature exclusive to Pro-subscribers (and often to players in a Game created by a Pro-subscriber). If you'd like to use this feature, consider upgrading your account. |
Main Article: Building Character Sheets
Learning CSS and HTML can be daunting. And yet, if you want to create a character sheet for Roll20, both are required.
Character Sheet Development
Getting Started
- Using Custom Sheets
- Building Sheets
(Main Page) - Glossary
- Code Restrictions
- Best Practice
- Common Mistakes
- Tutorials
- Examples, Templates
- Pattern Libraries
- HTML & storing data
- CSS & Styling
General
- Updates & Changelog
- Known Bugs
- Character Sheet Enhancement(CSE)
- Custom Roll Parsing
- Legacy Sheet(LCS)
- Beacon SDK
Reference
- Buttons
- Repeating Sections
- Sheetworkers
- Roll Templates
- sheet.json
- Translation
- Auto-Calc
- Advanced
- All SheetDev Pages
Tools & Tips
Other
This page gives a number of examples for creative way to leverage the character sheet system. Most of these tips involve CSS and were pulled from the CSS Wizardry thread in the Character Sheets forum, but some tips don't necessarily involve CSS at all.
These tips & examples are made to work with Legacy Sheet, so need adjustments to work with newer CSE-sheets. See the Character Sheet Enhancement-page for more info. Andreas J. (talk) 16:21, 15 April 2021 (UTC) |
Many of these tips also include links to a live demo on JSFiddle, so you can see them in action.
For other pages related to CSS & Design for Roll20 character sheets, see:
- Designing Character Sheet Layout - tips on how to best design the broad strokes of a character sheet
- Image use in character sheets - How to include images on your character sheets
- Sheet Design Best Practices
Contents |
Show/Hide Areas
Common mistake:: Not understanding how General Sibling Selector ~ works, and how it applies in making tabs/hideable areas on the sheet. The examples on show/hide areas & creating tabs relies on the correct positioning of elements, and if the html elements are thrown in a different order or withing other elements, the conditions aren't met for things to trigger. |
You can show or hide areas on the character sheet based on the state of a checkbox. Instead of the adjacent sibling selector (+
) used by custom checkboxes, you should use the sibling selector (~
).
On the 'toggle-show' checkbox, the following 'body' is hidden when the toggle is not checked.
On the 'toggle-hide' checkbox, the following 'body' is hidden when the toggle is checked.
<div class="sheet-columns"> <div> <input type="checkbox" class="sheet-toggle-show" /> <span>Show</span> <div class="sheet-body">You found me!</div> </div> <div> <input type="checkbox" class="sheet-toggle-hide" /> <span>Hide</span> <div class="sheet-body">Hey, a little privacy here?!</div> </div> </div>
.sheet-columns { display: flex; justify-content: space-between; width: 400px; } .sheet-columns > * { flex: 1; } input.sheet-toggle-show:not(:checked) ~ div.sheet-body, input.sheet-toggle-hide:checked ~ div.sheet-body { display: none; }
Swap Visible Areas
You can apply the hide areas logic to multiple elements based on the same checkbox, and get swappable behavior:
<div> <input type="checkbox" class="sheet-block-switch" name="attr_block_switch" value="1"> <div class="sheet-block-a"> Lorem ipsum dolor sit amet </div> <div class="sheet-block-b"> consectetur adipiscing elit </div> </div>
.sheet-block-a, .sheet-block-switch:checked ~ .sheet-block-b { display: block; } .sheet-block-b, .sheet-block-switch:checked ~ .sheet-block-a { display: none; }
Tabs
Many paper-versions of character sheets have more than one page, and among the best ways to implement this in Roll20 sheets is to create tabs for each page, swapping the visible area with some form of buttons. The following examples are the main two ways this can be done, where the first one is more streamlined than the older example, and is using much more of an modern approach, leading to less problems on older/less used browsers.
Button-based tabs
This is a short and simple example of implementing tabs on a character sheet using action buttons and sheetworkers, created by Finderski & GiGs.
<input type="hidden" class="sheet-tabstoggle" name="attr_sheetTab" value="character" /> <div> <button type="action" name="act_character" >Character</button> <button type="action" name="act_journal" >Journal</button> <button type="action" name="act_configuration" >Configuration</button> </div> <div class="sheet-character"> <h2>Character</h2> <span>character Stuff Goes here </span> </div> <div class="sheet-journal"> <h2>Journal/Notes</h2> <span>Journal/Notes Stuff Goes here</span> </div> <div class="sheet-config"> <h2>Config/Settings</h2> <span>Sheet Config/Settings goes here</span> </div> <script type="text/worker"> const buttonlist = ["character","journal","configuration"]; buttonlist.forEach(button => { on(`clicked:${button}`, function() { setAttrs({ sheetTab: button }); }); }); </script>
/*Configure the tab buttons*/ .sheet-character, .sheet-config, .sheet-journal { display: none; } /* show the selected tab */ .sheet-tabstoggle[value="character"] ~ div.sheet-character, .sheet-tabstoggle[value="configuration"] ~ div.sheet-config, .sheet-tabstoggle[value="journal"] ~ div.sheet-journal { display: block; }
radio input + span .method
An Older Tab Example using invisible radio inputs + spans to create the "buttons"
Note: This example/method of creating tabs on a character sheet does not work out of the box, contains lots of unnecessary code, and is far less intuitive than the new tab example. This example is generally not recommended to use, but is left here just for the sake of continuity, and those who might need troubleshoot their implementation. Ambition & Avarice and Hc Svnt Dracones 2E are two sheets that base their tabs on this older "invisible radio input + span"-method, and both needed manual adjustment on at least the position of the tabs to actually work.
Old sheets with bad tabs: Lastly, a number of older sheet's using a variation of this older "input + span"-method don't render correctly on Firefox, and instead shows just the larger radio buttons, but doesn't show the name of the tab. These currently broken tab-implementations could be changed to use either this working version of the "input + span"-method or to the newer "action button" example.
- I suspect these older "input + span" implementations where broken by some minor change that Firefox did to how their display certain elements, and due to this method being IMO a pretty ugly hack codewise that people wouldn't probably use on normal HTML/CSS, it's understandable our "hack" eventually stopped working. Andreas J. (talk) 12:11, 17 May 2021 (UTC)
Old Tab example(Doesn't work as-is, lots of redundant code, not intuitive) |
Live demo A tabbed layout is essentially an extension of hidden areas, using radio inputs instead of checkbox inputs. <input type="radio" name="attr_tab" class="sheet-tab sheet-tab1" value="1" checked="checked"><span title="First Tab"></span> <input type="radio" name="attr_tab" class="sheet-tab sheet-tab2" value="2"><span title="Second Tab"></span> <input type="radio" name="attr_tab" class="sheet-tab sheet-tab3" value="3"><span title="Third Tab"></span> <input type="radio" name="attr_tab" class="sheet-tab sheet-tab4" value="4"><span title="Fourth Tab"></span> <div class="sheet-tab-content sheet-tab1"> <h1>Tab 1</h1> Lorem ipsum dolor sit amet </div> <div class="sheet-tab-content sheet-tab2"> <h1>Tab the Second</h1> consectetur adipisicing elit </div> <div class="sheet-tab-content sheet-tab3"> <h1>3rd Tab</h1> sed do eiusmod tempor incididunt ut labore et dolore magna aliqua </div> <div class="sheet-tab-content sheet-tab4"> <h1>Fourth Tab</h1> Ut enim ad minim veniam </div> div.sheet-tab-content { display: none; } input.sheet-tab1:checked ~ div.sheet-tab1, input.sheet-tab2:checked ~ div.sheet-tab2, input.sheet-tab3:checked ~ div.sheet-tab3, input.sheet-tab4:checked ~ div.sheet-tab4 { display: block; } input.sheet-tab { width: 150px; height: 20px; position: relative; top: 5px; left: 6px; margin: -1.5px; cursor: pointer; z-index: 1; opacity: 0; } input.sheet-tab + span::before { content: attr(title); border: solid 1px #a8a8a8; border-bottom-color: black; text-align: center; display: inline-block; background: #fff; background: linear-gradient(to top, #c8c8c8, #fff, #c8c8c8); width: 150px; height: 20px; font-size: 18px; position: absolute; top: 12px; left: 13px; } input.sheet-tab:checked + span::before { background: #dcdcdc; background: linear-gradient(to top, #fcfcfc, #dcdcdc, #fcfcfc); border-bottom-color: #fff; } input.sheet-tab:not(:first-child) + span::before { border-left: none; } input.sheet-tab2 + span::before { background: #fee; background: linear-gradient(to top, #f8c8c8, #fee, #f8c8c8); left: 163px; } input.sheet-tab2:checked + span::before { background: #dcdcdc; background: linear-gradient(to top, #fcecec, #f8c8c8, #fcecec); border-bottom-color: #fcecec; } input.sheet-tab3 + span::before { left: 313px; } input.sheet-tab4 + span::before { left: 463px; } div.sheet-tab-content { border: 1px solid #a8a8a8; border-top-color: #000; margin: 2px 0 0 5px; padding: 5px; } div.sheet-tab2 { background-color: #fcecec; } The key to take away from this is that we have a set of radio buttons which are siblings to the divs that contain each tab's content. Then, we hide all of the tabs' content and use the sibling selector along with the The rest of this example shows off means to make your tabs look pretty, such as using |
Dropdown
Sometimes it makes more sense to hide/show areas of the sheet with a dropdown(<select>
), especially if there are lots of options, which would make buttons or radio inputs take too much space.
- Example: If the game ExampleRPG have three classes/archetypes, and each have very different looking sheets compared to the others, it would make sense to have a dropdown which you'd use to change which archetype's section is shown. If you put the whole code into the
character
-section of the above Tabs example, you could still have multiple pages for your sheet, where the other pages/tabs are identical, and only some of the content inside thecharacter
-page would change depending on the<select>
.
With the <select>
directly changing the values, there is no need for the sheetworker that was used in the Tabs example.
<div> <label>Archetype:<label> <select name="attr_archetype"> <option value="fighter" selected>fighter</option> <option value="mage">mage</option> <option value="rogue">rogue</option> </select> </div> <div class="character"> //remember that this works only if .sheet-archetypetoggle and .sheet.fighter, mage and rogue are siblings elements and put in the correct order <input type="hidden" class="sheet-archetypetoggle" name="attr_archetype" value="fighter" /> <input name="attr_character_name" type="text" /> <!-- name is shown regardless of selected archetype --> <div class="sheet-fighter"> <h2>fighter</h2> <span>Fighter weapon specialization here</span> </div> <div class="sheet-mage"> <h2>mage</h2> <span>Mage spellcasting & magic here</span> </div> <div class="sheet-rogue"> <h2>rogue</h2> <span>Rogue espertise and skill specialization here</span> </div> </div>
/* By deafult, hides all archetypes*/ .sheet-fighter, .sheet-mage, .sheet-rogue { display: none; } /* show the selected archetype */ .sheet-archetypetoggle[value="fighter"] ~ div.sheet-fighter, .sheet-archetypetoggle[value="mage"] ~ div.sheet-mage, .sheet-archetypetoggle[value="rogue"] ~ div.sheet-rogue { display: block; }
Cycling Button
It's sometimes useful to have a single control(like button/radio input) that the user can click on to cycle through a series of values/options, instead of creating a <select>
-dropdown.
Old example (pure html+css) |
You can't make a button that rotates through a list, changing a displayed value. However, you can fake it! The trick lies in layering the radio buttons on top of one another, and changing the z-index based on which input is checked.
When the first input is selected, only the second one will be visible, so you can't click on any other value. When the second is clicked, the third will become the only one you can click on, and so on. The user can also navigate back and forth using arrow keys, or a combination of the tab key (or shift+tab) and the space bar. <div class="sheet-damage-box"> <input type="radio" class="sheet-damage-box sheet-no-damage" name="attr_damage-box" value="0" checked> <input type="radio" class="sheet-damage-box sheet-bashing-damage" name="attr_damage-box" value="1"> <input type="radio" class="sheet-damage-box sheet-lethal-damage" name="attr_damage-box" value="2"> <input type="radio" class="sheet-damage-box sheet-aggravated-damage" name="attr_damage-box" value="3"> <span class="sheet-damage-box sheet-no-damage">☐ (no damage)</span> <span class="sheet-damage-box sheet-bashing-damage"> / (bashing damage)</span> <span class="sheet-damage-box sheet-lethal-damage">☓ (lethal damage)</span> <span class="sheet-damage-box sheet-aggravated-damage">✱ (aggravated damage)</span> </div> div.sheet-damage-box { width: 195px; height: 30px; position: relative; } input.sheet-damage-box { width: 30px; height: 30px; position: absolute; z-index: 1; } span.sheet-damage-box { margin: 10px 0 0 40px; display: none; } input.sheet-no-damage { z-index: 2; } input.sheet-no-damage:checked + input.sheet-bashing-damage, input.sheet-bashing-damage:checked + input.sheet-lethal-damage, input.sheet-lethal-damage:checked + input.sheet-aggravated-damage { z-index: 3; } input.sheet-no-damage:checked ~ span.sheet-no-damage, input.sheet-bashing-damage:checked ~ span.sheet-bashing-damage, input.sheet-lethal-damage:checked ~ span.sheet-lethal-damage, input.sheet-aggravated-damage:checked ~ span.sheet-aggravated-damage { display: inline-block; } As you can see, this uses the show/hide areas technique to display a span with some text for each radio input. You could also display an image, an input field, an entire section of the charactersheet, whatever you like. One fancy option would be to combine this with the technique to style your radio buttons. Hide the radios with |
This uses the same approach described in Checkbox and Radio Input Styling: it uses a hidden attribute to store the value and a styled input/button to update the value.
This trick is used on multiple WoD sheets to cycle through showing different types of damage symbols in their health boxes.
The key distinction here is incrementing the numeric value by 1 each time the button is clicked and using the Modulo Operator to cycle the value back to 0 once it reaches 4. This makes it cycle through 0, 1, 2, and 3 before going back to 0.
<div> <input type="hidden" name="attr_health_1" class="health" value="0" /> <button type="action" name="act_health_1_cycle" class="health"></button> <span class="no-damage">☐ (no damage)</span> <span class="bashing-damage"> / (bashing damage)</span> <span class="lethal-damage">☓ (lethal damage)</span> <span class="aggravated-damage">✱ (aggravated damage)</span> </div> <script type="text/worker"> on(`clicked:health_1_cycle`, () => { // Check the current value of the hidden attribute. getAttrs(["health_1"], (v) => { const healthValue = parseInt(v["health_1"]) || 0; // Increment the attribute value by 1, or cycle back to 0 if the incremented value is equal to 4. setAttrs({ "health_1": (healthValue + 1) % 4 }); }); }); </script>
/* Configure the button styling. This example makes it look like a checkbox. */ button.sheet-health { width: 10px; height: 10px; padding: 0; border: solid 1px #000000; background: #efefef; } /* Draw a diagonal slash if the hidden attribute value is "1" */ input.sheet-health[value="1"] + button.sheet-health { background: linear-gradient(to top left, #efefef 0%, #efefef calc(50% - 0.8px), rgba(0,0,0,1) 50%, #efefef calc(50% + 0.8px), #efefef 100%); } /* Draw two diagonal slashes if the hidden attribute value is "2" */ input.sheet-health[value="2"] + button.sheet-health { background: linear-gradient(to top left, rgba(0,0,0,0) 0%, rgba(0,0,0,0) calc(50% - 0.8px), rgba(0,0,0,1) 50%, rgba(0,0,0,0) calc(50% + 0.8px), rgba(0,0,0,0) 100%), linear-gradient(to top right, #efefef 0%, #efefef calc(50% - 0.8px), rgba(0,0,0,1) 50%, #efefef calc(50% + 0.8px), #efefef 100%); } /* Draw three diagonal slashes if the hidden attribute value is "3" */ input.sheet-health[value="3"] + button.sheet-health { background: linear-gradient(to top left, rgba(0,0,0,0) 0%, rgba(0,0,0,0) calc(50% - 0.8px), rgba(0,0,0,1) 50%, rgba(0,0,0,0) calc(50% + 0.8px), rgba(0,0,0,0) 100%), linear-gradient(to top right, rgba(0,0,0,0) 0%, rgba(0,0,0,0) calc(50% - 0.8px), rgba(0,0,0,1) 50%, rgba(0,0,0,0) calc(50% + 0.8px), rgba(0,0,0,0) 100%), linear-gradient(to right, #efefef 0%, #efefef calc(50% - 0.8px), rgba(0,0,0,1) 50%, #efefef calc(50% + 0.8px), #efefef 100%); } /* Hide content that does not correspond with the hidden attribute's value. */ input.sheet-health[value="0"] ~ :not(.sheet-health):not(.sheet-no-damage) { display: none; } input.sheet-health[value="1"] ~ :not(.sheet-health):not(.sheet-bashing-damage) { display: none; } input.sheet-health[value="2"] ~ :not(.sheet-health):not(.sheet-lethal-damage) { display: none; } input.sheet-health[value="3"] ~ :not(.sheet-health):not(.sheet-aggravated-damage) { display: none; }
Four Ways to Use an Attribute
Standard
Create one of: <input>
(with a type
attribute of "text", "number", "checkbox", "radio", or "hidden"), <select>
, or <textarea>
, and set the element's name
attribute to a value beginning with "attr_"
<input type="text" name="attr_text_example"> <input type="number" name="attr_number_example"> <input type="checkbox" name="attr_checkbox_example"> <input type="radio" name="attr_radio_example"> <input type="hidden" name="attr_hidden_example" value="0"> <select name="attr_select_example"> <option value="1">First option</option> <option value="2">Second option</option> </select> <textarea name="attr_textarea_example"></textarea>
The value of the form element will be stored as the value of an attribute with the same name as the form element, except the "attr_" prefix will be removed. So, an element named "attr_example" will be stored in the attribute "example".
Text inputs, number inputs, and textareas will not update the backing attribute until they lose focus, for example when you click elsewhere on the sheet or hit the [tab] key.
Hidden inputs are, as you might guess, hidden to the user. They cannot be interacted with, and so they are prime candidates for intermediate calculations of autocalc or storing things the user doesn't need to see or change for Sheet Worker Scripts.
If you have multiple radio inputs with the same name, only one of those radios will be checked at any given time. If you have multiple other kinds of elements with the same name, their values will be synchronized. This can be used, for example, if you have a tab layout with the same field present in two tabs. Give them both the same name, and they will always have the same value.
Autocalc Fields
Main Page: Auto-Calc
If a field has the disabled
attribute, the user will be unable to modify its value and its value will be treated as a mathematical equation (which can reference other attributes of the character). The result of that formula will be what the user sees. Errors in the formula (for example, @{a} + @{b} + @{c}
when attribute b has no value) will result in no output.
When using sheet workers, the value of the autocalc field you get from the getAttrs
function will be its formula, and you cannot set its value to something else. See sheetworker-autocalc for a utility to resolve autocalc fields to their calculated value in a sheet worker script. Note: sheetworker-autocalc has not been tested with repeating fields.
Readonly Fields
If a field has the readonly
attribute, the user will be unable to modify its value, and its default styling will be the same as if it were disabled. However, sheet worker scripts will be able to modify its value, and if its value is some kind of equation, it won't be automatically calculated. Note: If for some reason a readonly
field is an equation, sheetworker-autocalc will be able to resolve it to a value just fine.
Attribute-Backed <span>s
A <span>
element can be given an attr_
name, just like one of the form elements, above. This will cause the span to behave similarly to a readonly
field in that the user cannot modify it directly, and sheet worker scripts have no trouble doing so. There are two main differences with an attribute backed span:
- The default styling: the span will look just like the surrounding text
- When using attribute backed spans in a repeating section, you should always make a hidden input version of the attribute as attribute backed spans in repeating sections cannot be called without the full repeating section syntax (e.g.
@{repeating_SECTIONNAME_$X_attribute_name}
).
Checkbox and Radio Input Styling
Checkboxes and radio buttons don't like getting changed much. Instead, it can be easier to use a hidden attribute and present a button to update the attribute.
New Pure HTML + CSS styling with Labels
This is Richard T's updated pure HTML + CSS that uses labels to force interior elements to be part of the input.
https://jsfiddle.net/medieve/rfkvu3hm/
Old Checkbox / Radio example (pure html+css, but less intuitive and difficult to align) |
Note: This example/method of creating styled checkboxes or radios on a character sheet does not work out of the box, contains lots of unnecessary code, and is far less intuitive than the new button example. This example is generally not recommended to use, but is left here just for the sake of continuity, and those who might need troubleshoot their implementation. Live Demo For a pure html+css approach with no sheet worker scripts, you can make the checkbox/radio invisible (but still clickable!) and overlay it on top of a more cooperative element. <input type="checkbox"><span></span> <input type="radio" name="attr_r"><span></span> <input type="radio" name="attr_r"><span></span> <input type="radio" name="attr_r"><span></span> <input type="radio" name="attr_r"><span></span> <input type="radio" name="attr_r"><span></span> /* Hide actual radio/checkbox */ input[type="radio"], input[type="checkbox"] { opacity: 0; width: 16px; height: 16px; position: relative; top: 5px; left: 6px; margin: -10px; cursor: pointer; z-index: 1; } /* Fake radio/checkbox */ input[type="radio"] + span::before, input[type="checkbox"] + span::before { margin-right: 4px; border: solid 1px #a8a8a8; line-height: 14px; text-align: center; display: inline-block; vertical-align: middle; box-shadow: 0 0 2px #ccc; background: #f6f6f6; background: radial-gradient(#f6f6f6, #dfdfdf); } /* Fake radio */ input[type="radio"] + span::before { content: ""; width: 12px; height: 12px; font-size: 24px; border-radius: 50%; } input[type="radio"]:checked + span::before { content: "•"; } /* Fake checkbox */ input[type="checkbox"] + span::before { content: ""; width: 14px; height: 14px; font-size: 12px; border-radius: 3px; } input[type="checkbox"]:checked + span::before { content: "✓"; } The key here is the Note: Because of the way this is set up, if you do not have a span element immediately following your checkbox/radio button, the checkbox/radio button will not be visible. |
(The following examples use CSS flex for alignment. For more on flex layouts, see Building Character Sheets#Layout)
Checkbox Example
<div class="toggle-container"> <input type="hidden" class="toggle" name="attr_show_flag" /> <button type="action" name="act_show" class="toggle"> <span class="checked">✓</span> </button> <span>Show?</span> </div> <script type="text/worker"> // Register the click handler to all specified buttons. const toggleList = ["show"]; toggleList.forEach(function(button) { on(`clicked:${button}`, function() { const flag = `${button}_flag`; // Check the current value of the hidden flag. getAttrs([flag], function(v) { // Update the value of the hidden flag to "1" for checked or "0" for unchecked. setAttrs({ [flag]: v[flag] !== "1" ? "1" : "0" }); }); }); }); </script>
/* Configure a container for the toggle */ .sheet-toggle-container { display: inline-flex; align-items: center; } /* Configure the button styling. This example makes it look like a checkbox. */ button.sheet-toggle { padding: 0; border: solid 1px #a8a8a8; cursor: pointer; width: 16px; height: 16px; border-radius: 3px; display: flex; justify-content: center; align-items: center; font-size: 12px; } /* Hide the "checked" section of the toggle if the attribute value is not "1". */ input.sheet-toggle:not([value="1"]) ~ button.sheet-toggle > span.sheet-checked { display: none; }
Radios Example
<div class="radios"> <input type="hidden" class="radio" name="attr_level" value="1" /> <button type="action" name="act_level_1" class="radio radio-1"> <span class="checked"></span> </button> <button type="action" name="act_level_2" class="radio radio-2"> <span class="checked"></span> </button> <button type="action" name="act_level_3" class="radio radio-3"> <span class="checked"></span> </button> <button type="action" name="act_level_4" class="radio radio-4"> <span class="checked"></span> </button> <button type="action" name="act_level_5" class="radio radio-5"> <span class="checked"></span> </button> </div> <script type="text/worker"> const levelRadioValues = ["1","2","3","4","5"]; levelRadioValues.forEach(function(value) { on(`clicked:level_${value}`, function() { setAttrs({ ["level"]: value }); }); }); </script>
/* Configure a container for the radio buttons. */ .sheet-radios { display: flex; align-items: center; } /* Configure the button styling. This example makes it look like a radio. */ button.sheet-radio { padding: 0; border: solid 1px #a8a8a8; cursor: pointer; width: 14px; height: 14px; border-radius: 50%; display: flex; justify-content: center; align-items: center; } button.sheet-radio > span.sheet-checked { width: 6px; height: 6px; border-radius: 50%; background: buttontext; } /* Hide the "checked" section of the radio if the attribute value does not match the radio */ input.sheet-radio:not([value="1"]) ~ button.sheet-radio-1 > span.sheet-checked, input.sheet-radio:not([value="2"]) ~ button.sheet-radio-2 > span.sheet-checked, input.sheet-radio:not([value="3"]) ~ button.sheet-radio-3 > span.sheet-checked, input.sheet-radio:not([value="4"]) ~ button.sheet-radio-4 > span.sheet-checked, input.sheet-radio:not([value="5"]) ~ button.sheet-radio-5 > span.sheet-checked { display: none; }
Alternative Checkboxes
You're not restricted to a box with a check on it if you want a binary state (on or off). When styling your checkbox (or radio button!) you can use just about anything.
Old example (pure html+css) |
/* Fake checkbox */ input[type="checkbox"] + span::before { margin-right: 4px; line-height: 14px; text-align: center; display: inline-block; vertical-align: middle; content: "▼"; width: 14px; height: 14px; font-size: 12px; } input[type="checkbox"]:checked + span::before { content: "►"; } You can also use an image instead of a string, such as |
<input type="hidden" class="direction" name="attr_direction" value="down" /> <button type="action" name="act_direction_toggle" class="direction"> <span class="down">▼</span> <span class="right">►</span> </button> <script type="text/worker"> on("clicked:direction_toggle", function() { // Check the current value of the hidden attribute. getAttrs(["direction"], function(v) { // Toggle the hidden attribute value between "down" and "right" setAttrs({ "direction": v["direction"] !== "down" ? "down" : "right" }); }); }); </script>
/* Clear default button styling */ button.sheet-direction { margin: 0; overflow: visible; text-transform: none; border-style: none; padding: 0; background: transparent; cursor: pointer; } /* Hide the section(s) that do not match the attribute value */ input.sheet-direction:not([value="down"]) + button.sheet-direction > span.sheet-down { display: none; } input.sheet-direction:not([value="right"]) + button.sheet-direction > span.sheet-right { display: none; }
Now, instead of an empty box, or a box with a checkmark, you've got a right-pointing arrow or a down-pointing arrow. You can hide/display any other html content based on the hidden attribute value.
Fill Radio Buttons to the Left
A number of games use a set of bubbles, filled in from left to right, to represent various traits. For example, selecting the third bubble in "Strength" to indicate a Strength value of 3 should also fill in bubbles 1 and 2.
Old example (pure html+css) |
Radio buttons can only have one selected value, however, and if we used a set of checkboxes, it would be annoying to make the user click each and every one of them to set the character's attribute. Also, a set of checkboxes would make macros extremely ugly: However, with the radio button styling, we can solve this problem and use a radio button anyway, and only have one value. <input type="radio" name="attr_r" value="1" checked="checked"><span></span> <input type="radio" name="attr_r" value="2"><span></span> <input type="radio" name="attr_r" value="3"><span></span> <input type="radio" name="attr_r" value="4"><span></span> <input type="radio" name="attr_r" value="5"><span></span> /* Hide actual radio */ input[type="radio"] { opacity: 0; width: 16px; height: 16px; position: relative; top: 5px; left: 6px; margin: -10px; cursor: pointer; z-index: 1; } /* Fake radio */ input[type="radio"] + span::before { margin-right: 4px; border: solid 1px #a8a8a8; line-height: 14px; text-align: center; display: inline-block; vertical-align: middle; box-shadow: 0 0 2px #ccc; background: #f6f6f6; background: radial-gradient(#f6f6f6, #dfdfdf); content: "•"; width: 12px; height: 12px; font-size: 24px; border-radius: 50%; } /* Remove dot from all radios _after_ selected one */ input[type="radio"]:checked ~ input[type="radio"] + span::before { content: ""; } Here, all radio buttons are styled by default to appear as though they're checked. The radio buttons after the one that's actually checked then have the dot removed. The result is that the checked radio button and all of the ones to the left are "filled in," while the ones to the left are empty. You can invert this behavior (right of the checked radio are filled, checked and left of checked are empty) by swapping the two To reverse this behavior (checked radio and right of checked radio are filled, left of checked radio are empty), swap the two input[type="radio"]:checked ~ input[type="radio"] + span::before, input[type="radio"]:checked + span::before Note: If no radio button is selected, all of them will appear filled in (or all will appear empty if you've reversed/inverted the CSS). Therefore, it is wise to include Note: All radio buttons which are siblings will be affected by the selection of one of the radios. It is therefore recommended that you wrap the button group in some element, such as span or div. |
<div class="dots"> <input type="hidden" name="attr_strength" class="dot" value="1" /> <button type="action" name="act_strength_1" class="dot"> <span class="checked"></span> </button> <button type="action" name="act_strength_2" class="dot gt-1"> <span class="checked"></span> </button> <button type="action" name="act_strength_3" class="dot gt-1 gt-2"> <span class="checked"></span> </button> <button type="action" name="act_strength_4" class="dot gt-1 gt-2 gt-3"> <span class="checked"></span> </button> <button type="action" name="act_strength_5" class="dot gt-1 gt-2 gt-3 gt-4"> <span class="checked"></span> </button> </div> <script type="text/worker"> const strengthValues = ["1","2","3","4","5"]; strengthValues.forEach(function(value) { on(`clicked:strength_${value}`, function() { setAttrs({ "strength": value }); }); }); </script>
.sheet-dots{ display:flex; } /* Configure the button styling. This example makes it look like a radio. */ button.sheet-dot { padding: 0; border: solid 1px #a8a8a8; cursor: pointer; width: 14px; height: 14px; border-radius: 50%; display: flex; justify-content: center; align-items: center; } button.sheet-dot > span { width: 6px; height: 6px; border-radius: 50%; background: buttontext; } /* Hide the "checked" section of the radio if the hidden attribute value is greater than the button value */ input.sheet-dot[value="1"] ~ button.sheet-gt-1 > span.sheet-checked { display: none; } input.sheet-dot[value="2"] ~ button.sheet-gt-2 > span.sheet-checked { display: none; } input.sheet-dot[value="3"] ~ button.sheet-gt-3 > span.sheet-checked { display: none; } input.sheet-dot[value="4"] ~ button.sheet-gt-4 > span.sheet-checked { display: none; }
Here, the gt-*
classes are used to indicate "greater than" a particular value. This doesn't use any kind of math, so all distinct "gt-*" classes have to be included. For example, dot 3 has classes "gt-1" and "gt-2" because 3 is greater than both 1 and 2. Dot 1 does not have any "gt-*" classes because it is not greater than any of the other options. (If a zero option is possible, then all of these buttons will need a "gt-0" class.)
Each possible value of the attribute needs a corresponding CSS rule to hide all values greater than that value. (The value of "5" doesn't need a rule here because there are no options greater than 5.)
This example uses a subtractive approach, meaning a that by default the button will indicate it is checked unless a CSS rule hides the "checked" span.
Important Note: When using this trick inside a repeating section, it will not work if there are underscores in an action button name. The above buttons should be named "attr_strength1" or "attr_strength-1" - they cant be "attr_strength_1".
Circular Layouts
Some character sheets have rather interesting layouts. Mage: the Ascension, for example, has a pair of traits called Quintessence and Paradox that are both mapped onto a wheel of checkboxes.
<div> <div> <input type="checkbox" class="sheet-wheel sheet-wheel9 sheet-middle sheet-left-1" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel27 sheet-mid-three-eighth sheet-left-2" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel45 sheet-mid-quarter sheet-left-3" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel63 sheet-mid-eighth sheet-left-4" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel81 sheet-top sheet-left-5" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel99 sheet-top sheet-left-6" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel117 sheet-mid-eighth sheet-left-7" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel135 sheet-mid-quarter sheet-left-8" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel153 sheet-mid-three-eighth sheet-left-9" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel171 sheet-middle sheet-left-10" value="1"><span></span> </div> <div> <input type="checkbox" class="sheet-wheel sheet-wheel171 sheet-middle-2 sheet-left-1" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel153 sheet-mid-five-eighth sheet-left-2" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel135 sheet-mid-three-quarter sheet-left-3" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel117 sheet-mid-seven-eighth sheet-left-4" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel99 sheet-bottom sheet-left-5" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel81 sheet-bottom sheet-left-6" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel63 sheet-mid-seven-eighth sheet-left-7" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel45 sheet-mid-three-quarter sheet-left-8" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel27 sheet-mid-five-eighth sheet-left-9" value="1"><span></span> <input type="checkbox" class="sheet-wheel sheet-wheel9 sheet-middle-2 sheet-left-10" value="1"><span></span> </div> <div class="sheet-marker">•</div> </div>
/* Hide actual checkbox */ input[type="checkbox"] { position: absolute; opacity: 0; width: 15px; height: 15px; cursor: pointer; z-index: 1; margin-top: 6px; } /* Fake checkbox */ input[type="checkbox"] + span::before { border: solid 1px #a8a8a8; line-height: 14px; text-align: middle; display: inline-block; vertical-align: middle; box-shadow: 0 0 2px #ccc; background: #f6f6f6; background: radial-gradient(#f6f6f6, #dfdfdf); position: relative; content: ""; width: 14px; height: 14px; font-size: 12px; border-radius: 3px; } /* Styles unique to fake checkbox (checked) */ input[type="checkbox"]:checked + span::before { content: "✓"; color: #a00; box-shadow: 0 0 2px transparent; } /* Position checkboxes vertically in circle */ input.sheet-top { overflow:auto;white-space:pre-wrap;; } input.sheet-top + span::before { top: 0px; } input.sheet-mid-eighth { margin-top: 12px; } input.sheet-mid-eighth + span::before { top: 7px; } input.sheet-mid-quarter { margin-top: 27px; } input.sheet-mid-quarter + span::before { top: 22px; } input.sheet-mid-three-eighth { margin-top: 45px; } input.sheet-mid-three-eighth + span::before { top: 40px; } input.sheet-middle { margin-top: 67px; } input.sheet-middle + span::before { top: 62px; } input.sheet-middle-2 { margin-top: 73px; } input.sheet-middle-2 + span::before { top: 68px; } input.sheet-mid-five-eighth { margin-top: 95px; } input.sheet-mid-five-eighth + span::before { top: 90px; } input.sheet-mid-three-quarter { margin-top: 113px; } input.sheet-mid-three-quarter + span::before { top: 108px; } input.sheet-mid-seven-eighth { margin-top: 127px; } input.sheet-mid-seven-eighth + span::before { top: 122px; } input.sheet-bottom { margin-top: 135px; } input.sheet-bottom + span, input.sheet-bottom + span::before { top: 130px; } /* Position checkboxes horizontally in circle */ input.sheet-left-1 { margin-left: 14px; } input.sheet-left-1 + span::before { left: 14px; } input.sheet-left-2 { margin-left: 1px; } input.sheet-left-2 + span::before { left: 1px; } input.sheet-left-3 { margin-left: -4px; } input.sheet-left-3 + span::before { left: -4px; } input.sheet-left-4 { margin-left: -5px; } input.sheet-left-4 + span::before { left: -5px; } input.sheet-left-5 { margin-left: -2px; } input.sheet-left-5 + span::before { left: -2px; } input.sheet-left-6 { margin-left: 1px; } input.sheet-left-6 + span::before { left: 1px; } input.sheet-left-7 { margin-left: 3px; } input.sheet-left-7 + span::before { left: 3px; } input.sheet-left-8 { margin-left: 2px; } input.sheet-left-8 + span::before { left: 2px; } input.sheet-left-9 { margin-left: -4px; } input.sheet-left-9 + span::before { left: -4px; } input.sheet-left-10 { margin-left: -16px; } input.sheet-left-10 + span::before { left: -16px; } /* Rotate checkboxes */ input.sheet-wheel9, input.sheet-wheel9 + span::before { transform: rotate(9deg); } input.sheet-wheel27, input.sheet-wheel27 + span::before { transform: rotate(27deg); } input.sheet-wheel45, input.sheet-wheel45 + span::before { transform: rotate(45deg); } input.sheet-wheel63, input.sheet-wheel63 + span::before { transform: rotate(63deg); } input.sheet-wheel81, input.sheet-wheel81 + span::before { transform: rotate(81deg); } input.sheet-wheel99, input.sheet-wheel99 + span::before { transform: rotate(99deg); } input.sheet-wheel117, input.sheet-wheel117 + span::before { transform: rotate(117deg); } input.sheet-wheel135, input.sheet-wheel135 + span::before { transform: rotate(135deg); } input.sheet-wheel153, input.sheet-wheel153 + span::before { transform: rotate(153deg); } input.sheet-wheel171, input.sheet-wheel171 + span::before { transform: rotate(171deg); } div.sheet-marker { margin: 36px 0px 0px 5px; font-size: 20px; }
Select Dropdown Styling
<select>
elements are notoriously difficult to apply most styles to. However, using :hover
pseudo-selectors and radio buttons, you can create something approximating a dropdown with whatever style you like.
<div class="sheet-container"> <div class="sheet-child"> <input type="radio" name="attr_radio" class="sheet-select-radio sheet-d4" value="1" checked="true" /> <label>d4</label> <input type="radio" name="attr_radio" class="sheet-select-radio sheet-d8" value="2" /> <label>d8</label> <div class="sheet-d4"></div> <div class="sheet-d8"></div> </div> </div>
.sheet-container { width: 280px; } .sheet-container, .sheet-child { display: inline-block; } .sheet-child { vertical-align: middle; width: 35px; height: 35px; } .sheet-child input, .sheet-child input + label { display: none; z-index: 1; } .sheet-child:hover { background: gray; position:absolute; width: 100px; height: auto; z-index: 1; padding: 5px; } .sheet-child:hover > div.sheet-d4 { display: none; } .sheet-child:hover input, .sheet-child:hover input + label { display: inline; } .sheet-child:hover input + label { margin-right: 50% } .sheet-child:hover label { display: inline-block; } div.sheet-d4 { background-position: -411px -1px; width: 35px; height: 35px; background-image: url("https://i.imgur.com/zkgyBOi.png"); background-repeat: no-repeat; color: transparent; display: none; } div.sheet-d8 { background-position: -703px -1px; width: 35px; height: 35px; background-image: url("https://i.imgur.com/zkgyBOi.png"); background-repeat: no-repeat; color: transparent; display: none; } .sheet-child:not(:hover) input.sheet-select-radio.sheet-d4:checked ~ div.sheet-d4, .sheet-child:not(:hover) input.sheet-select-radio.sheet-d8:checked ~ div.sheet-d8 { display: block; }
Select Dropdown Text Styling
Here's another method to edit text within a select. Use an input with type="hidden" given the same attribute name as the select to trigger css styles on the select's option text.
<input name="attr_favcolor" type="hidden" class="sheet-color-switch"> <select name="attr_favcolor" class="sheet-color-select1"> <option style="color:red;" value="0" selected>Red</option> <option style="color:yellow;" value="1">Yellow</option> <option style="color:blue;" value="2">Blue</option> </select>
.sheet-color-switch[value="0"] + .sheet-color-select1 {color: red;} .sheet-color-switch[value="1"] + .sheet-color-select1 {color: yellow;} .sheet-color-switch[value="2"] + .sheet-color-select1 {color: blue;}
Optgroup
If optgroup doesn't work, one workaround is to add a styled, disabled <option>
in place of <optgroup>
-element to work in a similar way.
select/option can be hard to style, and sometimes browsers don't show the styling. In the example,
are used to indent the names, as padding & margin doesn't seem to work.
<select name="attr_ancestry"> <option disabled style="background:gray;font-weight:bold;color:white;"> MEN</option> <option value="Imperal">Imperal</option> <option value="Breton">Breton</option> <option value="Redguard">Redguard</option> <option value="Nord">Nord</option> <option disabled style="background:gray;font-weight:bold;color:white;"> MER</option> <option value="Altmer">Altmer</option> <option value="Bosmer">Bosmer</option> <option value="Dunmer">Dunmer</option> <option value="Orsmer">Orsmer</option> </select>
Button Styling
See the Styling Roll Buttons on the main Button-page for examples. The examples can be adjusted to work with Action and Compendium buttons.
How to remove the default d20 from roll buttons:
CSS:
button[type=roll].sheet-blank-button::before { content: ''; }
HTML:
<button class="sheet-blank-button" name="roll_BluffCheck" value="/roll 1d20 + @{Bluff}" type="roll">Bluff</button>
The d20 is a single character with the dicefontd20 font-family in the button's ::before
pseudo-element. Setting the content to an empty string removes it.
Styling Repeating Sections
main article: Styling Repeating Sections on the Repeating Sections-page
It's possible to style your repeating sections in a variety of ways. However, you can't just write your CSS as though the <fieldset>
that's in your HTML source is what the user is viewing, as roll20 creates their own elements and css for things that are inside.
you can do numerous things to alter how your repeating sections are displayed on the final character sheet. For example, you can have multiple repeating items per row:
.repcontainer[data-groupname="repeating_skills"] > .repitem { display: inline-block; }
Note: You do not prefix the rep* classes with sheet-
!
Remember to use the [data-groupname="repeating_..."]
attribute selector if you want to only apply the style to a single repeating section. Of course, if you want the style to affect all of your repeating sections, that's not needed.
What Can't You Do?
You cannot:
- Change the
display
property of the original<fieldset>
. - Change the text of the Add, Modify/Done, Delete, or Move buttons.
- However, you could set their opacity to 0 and display something in their place, much like styling checkboxes and radios, as well as add
::before
or::after
pseudo-elements to them.
- However, you could set their opacity to 0 and display something in their place, much like styling checkboxes and radios, as well as add
- Change the
display
property of the Add button after the user has pressed Modify once.
How they are rendered on the sheet
After writing the code for your repeating section on your html file, here is how it will look when rendered to the user:
<fieldset class="repeating_my-repeating-section" style="display: none;"> <!-- how the repating section code is structured in the browser after roll20 have created the parts inside it --> </fieldset> <div class="repcontainer" data-groupname="repeating_my-repeating-section"> <div class="repitem"> <div class="itemcontrol"> <button class="btn btn-danger pictos repcontrol_del">#</button> <a class="btn repcontrol_move">≡</a> </div> <!-- my-repeating-section HTML --> </div> <!-- there will be a div.repitem for each item the user has actually added to the sheet --> </div> <div class="repcontrol" data-groupname="repeating_my-repeating-section"> <button class="btn repcontrol_edit">Modify</button> <button class="btn repcontrol_add">+Add</button> </div>
When you click the Modify button, the Add button is is set to display: none
and the text of the Modify button is changed to "Done". When you click Done, the Add button is set to display: inline-block
and the text of the Done button is changed to "Modify". While modifying repitems, the repcontainer gains the class "editmode".
Content-scaled Inputs
By stacking your input with an attribute-backed span that defines the dimensions of the parent, you can create inputs that resize themselves to fit their text.
The html baseline is a simple parent container with your input and span inside:
<div class="autoExpand"> <input type="text" name="attr_expandText"> <span name="attr_expandText"></span> </div>
Here is the CSS with comments about what the properties are doing for you:
.autoExpand { overflow: hidden; /* hides overflow that is caused by the span */ position: relative; /* Allows the input's absolute positioning to be relative to this parent div */ min-width: 50px; /* Whatever feels good to you, prevents a new span from collapsing to 0. */ width: fit-content /* Will fit the width to the contents without going outside itself or collapsing smaller than its content, though we will be using min-width in the case where there's no content. */ height: 20px; /* Prevent the span from defining the height of the input container which can't be multiline */ } .autoExpand span { visibility: hidden; font-size: 1em; padding: 0 8px; /* Matching my input's padding so that the widths are correct */ } .autoExpand input { position: absolute; /* removes the input from the DOM flow, allowing the span to exist in the same space */ width: 100%; /* Inputs match the width of the parent element as defined by the spans */ font-size: 1em; }
Disable Click
You can use pointer-events or user-select to disable the user from selecting/interacting an element on the sheet with their mouse. It works for at least <select>
, text/number/checkbox/radio inputs
& <button>
.
Note: Neither pointer-events
nor user-select
stops users from selecting the element using the TAB-key.
.sheet-noclick{ pointer-events: none; }
Then you can use some other condition to turn this feature on/off, such as a checkbox. This trick have been used on many sheet.
Example:
<div> <label>disabled</label> <span> <input type="text" name="attr_name" class="noclick"> </span> <span> <input type="number" name="attr_lvl" class="noclick"> </span> <span> <input type="checkbox" name="attr_check" value="1" class="noclick"> </span> <span> <input type="radio" name="attr_radio" value="0" class="noclick"> <input type="radio" name="attr_radio" value="1" class="noclick"> </span> <select name="attr_WoundLevel" class="noclick"> <option value="0" selected="selected">Healthy</option> <option value="1">Stunned</option> <option value="1">Wounded</option> <option value="2">Wounded Twice</option> <option value="5">Incapacitated</option> <option value="10">Mortally Wounded</option> </select> <button type="roll" value="/roll 1d20" name="roll_init" class="noclick">init</button> </div> <div> <label>normal</label> <span> <input type="text" name="attr_name"> </span> <span> <input type="number" name="attr_lvl"> </span> <span> <input type="checkbox" name="attr_check" value="1"> </span> <span> <input type="radio" name="attr_radio" value="0"> <input type="radio" name="attr_radio" value="1"> </span> <select name="attr_class"> <option value="0" selected="selected">Warrior</option> <option value="1">Mage</option> <option value="2">Expert</option> </select> <button type="roll" value="/roll 1d20" name="roll_init">init</button> </div>
select.sheet-noclick, input[type="text"].sheet-noclick, input[type="number"].sheet-noclick, input[type="checkbox"].sheet-noclick, input[type="radio"].sheet-noclick, input[type="text"].sheet-noclick, button[type="roll"].sheet-noclick{ pointer-events: none; }
Dark/Light Mode
You can create an option for your sheet that can change the full color scheme of the sheet. The "Savage Worlds Tabbed" and "GURPS" sheet are two examples that have these options.
GiGs have made a short example of this.
With CSE, it's possible to have dark/light mode be automatically applied, based on the user's/computer light/dark preferences.
Custom Progress Bar
To create a custom progress bar, one can use CSS and sheetworkers to update an hidden field. See also: Creating a progress bar/health bar on the Character Sheet(Forum) for more examples.
Andreas J.'s example:<div class="health-bar"> <span>HP:</span> <input name="attr_hp" class="rangetest" type="range" min="0" max="10" value="10"> <input name="attr_hp" class="rangetest" type="number" value="10"> </div>
input[type="range"].sheet-rangetest{ width: 100px; } /* works only on firefox atm */ input[type="range"].sheet-rangetest::-moz-range-thumb{ background: transparent; border-color: transparent; color: transparent; } input[type="range"].sheet-rangetest::-moz-range-progress{ background: green; height: 10px; } input[type="range"].sheet-rangetest::-moz-range-track{ background: red; height: 10px; }
Leothedino's example – Live Demo
Leothedino have created a more sophisticated progress bar(Forum), where you can change the current and max value for what is displayed, and a sheetworker then scales the numbers to fit the bar.
<div class="sheet-container"> <input type="hidden" value="2" name="attr_Character_overweight" class="sheet-hidden sheet-overweight"> <div class="sheet-overweight"></div> </div>
div.sheet-overweight { width: 90%; height: 20px; border: 1px solid black; color: black; text-align: center; } input.sheet-overweight[value="0"] ~ div.sheet-overweight { background: white; } input.sheet-overweight[value="1"] ~ div.sheet-overweight { background: linear-gradient(to left, white 60%, green 75%); } input.sheet-overweight[value="2"] ~ div.sheet-overweight { background: linear-gradient(to left, white 40%, yellow 75%); } input.sheet-overweight[value="2"] ~ div.sheet-overweight:before { content:"Bags half full"; } input.sheet-overweight[value="3"] ~ div.sheet-overweight { background: linear-gradient(to left, white 20%, orange 75%); } input.sheet-overweight[value="3"] ~ div.sheet-overweight:before { content: "Bags nearly full"; } input.sheet-overweight[value="4"] ~ div.sheet-overweight { background: linear-gradient(to left, white, red 100%); } input.sheet-overweight[value="4"] ~ div.sheet-overweight:before { content: "Bags full !"; } input.sheet-overweight[value="5"] ~ div.sheet-overweight { background: black; color: white; } input.sheet-overweight[value="5"] ~ div.sheet-overweight:before { content: "Bags too heavy !"; }
Hexagons
<div class="sheet-hex sheet-hex-3" style="background-color: #444; width: 100px; height: 57px"> <div class="sheet-inner"> <h4>Stat</h4> <input type="number" style="width: 50%"> </div> <div class="sheet-corner-1"></div> <div class="sheet-corner-2"></div> </div>
.sheet-hex { width: 100px; height: 57px; background-color: #ccc; background-repeat: no-repeat; background-position: 50% 50%; background-size: auto 173px; position: relative; float: left; margin: 25px 5px; text-align: center; zoom: 1; } .sheet-hex.sheet-hex-gap { margin-left: 86px; } .sheet-hex a { display: block; width: 100%; height: 100%; text-indent: -9999em; position: absolute; top: 0; left: 0; } .sheet-hex .sheet-corner-1, .sheet-hex .sheet-corner-2 { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: inherit; z-index: -2; overflow: hidden; backface-visibility: hidden; } .sheet-hex .sheet-corner-1 { z-index:-1; transform: rotate(60deg); } .sheet-hex .sheet-corner-2 { transform: rotate(-60deg); } .sheet-hex .sheet-corner-1::before, .sheet-hex .sheet-corner-2::before { width: 173px; height: 173px; content: ''; position: absolute; background: inherit; top: 0; left: 0; z-index: 1; background: inherit; background-repeat: no-repeat; backface-visibility: hidden; } .sheet-hex .sheet-corner-1::before { transform: rotate(-60deg) translate(-87px, 0px); transform-origin: 0 0; } .sheet-hex .sheet-corner-2::before { transform: rotate(60deg) translate(-48px, -11px); bottom: 0; } /* Custom styles*/ .sheet-hex .sheet-inner { color: #eee; } .sheet-hex h4 { font-family: 'Josefin Sans', sans-serif; margin: 0; } .sheet-hex hr { border: 0; border-top: 1px solid #eee; width: 60%; margin: 15px auto; } .sheet-hex p { font-size: 16px; font-family: 'Kotta One', serif; width: 80%; margin: 0 auto; } .sheet-hex.sheet-hex-1 { background: #74cddb; } .sheet-hex.sheet-hex-2 { background: #f5c53c; } .sheet-hex.sheet-hex-3 { background: #80b971; }
Note: Make sure the hexagon's width is 57% of the hexagon's height.
Clocks
CSS gradients can do all kinds of interesting things. Here's an example of creating a clock-shaped representation of a value:
<div class="sheet-harm-section"> <input type="radio" value="0" name="attr_harm" class="sheet-harm-checkbox sheet-harm-0" checked>0 <input type="radio" value="1" name="attr_harm" class="sheet-harm-checkbox sheet-harm-3">3 <input type="radio" value="2" name="attr_harm" class="sheet-harm-checkbox sheet-harm-6">6 <input type="radio" value="3" name="attr_harm" class="sheet-harm-checkbox sheet-harm-9">9 <input type="radio" value="4" name="attr_harm" class="sheet-harm-checkbox sheet-harm-10">10 <input type="radio" value="5" name="attr_harm" class="sheet-harm-checkbox sheet-harm-11">11 <input type="radio" value="6" name="attr_harm" class="sheet-harm-checkbox sheet-harm12">12 <hr> <div class="sheet-clock"></div> </div>
.sheet-harm-section { display: inline-block; text-align: center; } .sheet-clock { width: 5em; height: 5em; display: inline-block; border-radius: 50%; background: black; border: 2px solid black; } .sheet-harm-checkbox.sheet-harm-0:checked ~ .sheet-clock { background: white; } .sheet-harm-checkbox.sheet-harm-3:checked ~ .sheet-clock { background-image: linear-gradient(180deg, transparent 50%, white 50%), linear-gradient(90deg, white 50%, transparent 50%); } .sheet-harm-checkbox.sheet-harm-6:checked ~ .sheet-clock { background-image: linear-gradient(90deg, white 50%, transparent 50%); } .sheet-harm-checkbox.sheet-harm-9:checked ~ .sheet-clock { background-image: linear-gradient(180deg, transparent 50%, black 50%), linear-gradient(90deg, white 50%, transparent 50%); } .sheet-harm-checkbox.sheet-harm-10:checked ~ .sheet-clock { background-image: linear-gradient(210deg, transparent 50%, black 50%), linear-gradient(90deg, white 50%, transparent 50%); } .sheet-harm-checkbox.sheet-harm-11:checked ~ .sheet-clock { background-image: linear-gradient(240deg, transparent 50%, black 50%), linear-gradient(90deg, white 50%, transparent 50%); } .sheet-harm-checkbox.sheet-harm-12:checked ~ .sheet-clock { background-color: black; }
Fonts
Roll20 supports five fonts (Arial, Patrick Hand, Contrail One, Shadows Into Light, and Candal) by default.
All the Websafe Fonts should also be available.
.charsheet span{ // defining more than one fint will only use the first one, but will fall back to the next font if the first one doesn't work for some reason. font-family: "Arial", "Patrick Hand"; }
Google Fonts
When creating or editing Legacy Sanitized sheets, follow the guide exactly to make it work, you can't use urls generated by google. All fonts must be loaded in a single @import . |
Bug: Seems impossible to use any font that includes eval in the name.discussion(Forum) Andreas J. (talk) 21:58, 10 March 2021 (UTC) |
Roll20 Character sheets supports importing fonts from Google Fonts. When creating sheets after the Character Sheet Enhancement, you can use the @import styles generated by google as-is, including weights and variations.
Roll20's mobile app currently has its own interesting quirks and steps to import fonts for mobile can be found here.
- Great Fonts for Character Sheets - Google Docs displaying a wide range of fonts, from medieval, futuristic, old western, to gothic & horror
The following instruction applies if you are using the Legacy Sanitation environment.
Roll20 legacy strictly uses the CSS-1 rule for importing fonts which is different from the auto-generated CSS-2 @import
rule that Google now uses (as of April 2020).
Examples of a Roll20 formatted import rules:
For the font "Sigmar One":
@import url('https://fonts.googleapis.com/css?family=Sigmar+One&display=swap');
or multiple font-families into Roll20, importing "Zilla Slab" and "Anton":
@import url('https://fonts.googleapis.com/css?family=Zilla+Slab|Anton&display=swap');
An example of an autogenerated @import rule by Google Fonts for "Anton" and "Roboto":
@import url('https://fonts.googleapis.com/css2?family=Anton&family=Roboto&display=swap');
A few things to point out that requires changes:
1. You will need to edit the css2 to css when importing fonts.
2. When bringing in multiple fonts, you will need to replace & with | and you do not need to repeat the family=
Roll20 does not support variable fonts, which is part of the CSS2 upgrade. It will bring in the font family Arvo, but will not bring in Arvo:ital@1. |
3. Some fonts may require a font weight. ie Open+Sans+Condensed:wght@300
For some reason this may not load in Roll20. Instead, try omitting wght@
from the snippet. example; Open+Sans+Condensed:300
Here is a quick template for use to construct your own rule.
@import url('https://fonts.googleapis.com/css?family=NAME+OF+FONT+YOU+WANT&display=swap');
You can then call the font from inside your CSS with the font-family
-attribute:
// imports the "Sigmar One" font to the sheet & rolltempltes @import url('https://fonts.googleapis.com/css?family=Sigmar+One&display=swap'); .charsheet span{ // sets all span to use "Sigmar One", and "Arial" is made a fallback font, if Sigmar doesn't work font-family: 'Sigmar One', Arial; }
For now, this is limited to Google Fonts, from fonts.googleapis.com
Video Guide to use Google Fonts by Stephanie
Custom Fonts
It's still possible to use custom fonts, but it's not guaranteed to work and requires the user who want to see them to disable browser brower security to allow "unsecure scripts" to either allow Roll20 to search for locally installed fonts or import a font from an URL.
Starfinder HUD is an example of a sheet using custom font, and having a fallback font if the user doesn't allow unsafe scripts.
@font-face{/*The aldrich font*/ font-family:Aldrich; src:local('Aldrich'),local('Aldrich-Regular'),url(https://kurohyou.github.io/Starfinder-Character-Sheet/Sheet%20Fonts/Aldrich-Regular.ttf); } .charsheet * { // sets Aldrich to be used on the whole sheet, & adds "Copperplate" & "Monospace" as backup fonts if Aldrich fails to load. font-family:Aldrich,Copperplate,Monospace; }
In the segment above, @font-face
defines the name and source of the font. It first asks the broswer if the font exists locally, and if it isn't locally installed, the url gives a location to download the url from.
Here is a list of Web-safe fonts that comes preinstalled on most operating systems, which helps avoiding the need to find a source where the font is hosted online. This search through Roll20's sheet repository shows other sheet that also use custom fonts.
Icon Fonts
An icon font is a font which has pictures instead of letters. You can specify one of the icon fonts below with the font-family
property. For example, something like:
<p>A Gem: <span style="font-family: 'Pictos Three'">a</span></p>
Would produce:
A Gem: a
Example:
.charsheet div.sheet-d6{ font-family: dicefontd6; // makes the content of this div a black d6 showing 6 dots, by defining the character that corresponds to that symbol. content: "L"; } .charsheet div.sheet-d8{ // any div with this class will display the test as various d8 dice font-family: dicefontd8; }
<div> <p>At character creation, you have a dicepool of:</p> <p>5 <div class="sheet-d6"></div> </p> <p>3 <div class="sheet-d8">h</div> </p> <p> bunch of d8 with different numbers<div class="sheet-d8">abcdefgh</div> </div>
Pictos
Character | Icon | Character | Icon | Character | Icon | Character | Icon | |||
---|---|---|---|---|---|---|---|---|---|---|
! | ! | : | : | S | S | l | l | |||
" | " | ; | ; | T | T | m | m | |||
# | # | < | < | U | U | n | n | |||
$ | $ | = | = | V | V | o | o | |||
% | % | > | > | W | W | p | p | |||
& | & | ? | ? | X | X | q | q | |||
' | ' | @ | @ | Y | Y | r | r | |||
( | ( | A | A | Z | Z | s | s | |||
) | ) | B | B | [ | [ | t | t | |||
* | * | C | C | \ | \ | u | u | |||
+ | + | D | D | ] | ] | v | v | |||
, | , | E | E | ^ | ^ | w | w | |||
- | - | F | F | _ | _ | x | x | |||
. | . | G | G | ` | ` | y | y | |||
/ | / | H | H | a | a | z | z | |||
0 | 0 | I | I | b | b | { | { | |||
1 | 1 | J | J | c | c | | | | | |||
2 | 2 | K | K | d | d | } | } | |||
3 | 3 | L | L | e | e | ~ | ~ | |||
4 | 4 | M | M | f | f | |||||
5 | 5 | N | N | g | g | |||||
6 | 6 | O | O | h | h | |||||
7 | 7 | P | P | i | i | |||||
8 | 8 | Q | Q | j | j | |||||
9 | 9 | R | R | k | k |
Pictos Custom
Character | Icon |
---|---|
[ | [ |
a | a |
e | e |
i | i |
o | o |
p | p |
q | q |
r | r |
t | t |
u | u |
w | w |
y | y |
Pictos Three
Character | Icon |
---|---|
a | a |
b | b |
c | c |
d | d |
e | e |
f | f |
g | g |
h | h |
i | i |
j | j |
k | k |
l | l |
dicefontd4
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | i | i | |
H | H | j | j | |
I | I | k | k | |
J | J | l | l | |
K | K | m | m | |
L | L | n | n | |
M | M | o | o | |
N | N | p | p | |
O | O | |||
P | P |
dicefontd6
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | i | i | |
H | H | j | j | |
I | I | k | k | |
J | J | l | l | |
K | K | m | m | |
L | L | n | n | |
M | M | o | o | |
N | N | p | p | |
O | O | q | q | |
P | P | r | r | |
Q | Q | |||
R | R |
dicefontd8
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | |||
H | H |
dicefontd10
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | i | i | |
H | H | j | j | |
I | I | k | k | |
J | J | l | l | |
K | K | m | m | |
L | L | n | n | |
M | M | o | o | |
N | N | p | p | |
O | O | q | q | |
P | P | r | r | |
Q | Q | s | s | |
R | R | t | t | |
S | S | |||
T | T |
dicefontd12
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | i | i | |
H | H | j | j | |
I | I | k | k | |
J | J | l | l | |
K | K | |||
L | L |
dicefontd20
Character | Icon | Character | Icon | |
---|---|---|---|---|
0 | 0 | a | a | |
@ | @ | b | b | |
A | A | c | c | |
B | B | d | d | |
C | C | e | e | |
D | D | f | f | |
E | E | g | g | |
F | F | h | h | |
G | G | i | i | |
H | H | j | j | |
I | I | k | k | |
J | J | l | l | |
K | K | m | m | |
L | L | n | n | |
M | M | o | o | |
N | N | p | p | |
O | O | q | q | |
P | P | r | r | |
Q | Q | s | s | |
R | R | t | t | |
S | S | |||
T | T |
dicefontd30
Character | Icon | Character | Icon | Character | Icon | ||
---|---|---|---|---|---|---|---|
0 | 0 | L | L | g | g | ||
1 | 1 | M | M | h | h | ||
2 | 2 | N | N | i | i | ||
3 | 3 | O | O | j | j | ||
4 | 4 | P | P | k | k | ||
5 | 5 | Q | Q | l | l | ||
6 | 6 | R | R | m | m | ||
7 | 7 | S | S | n | n | ||
8 | 8 | T | T | o | o | ||
@ | @ | U | U | p | p | ||
A | A | V | V | q | q | ||
B | B | W | W | r | r | ||
C | C | X | X | s | s | ||
D | D | Y | Y | t | t | ||
E | E | Z | Z | u | u | ||
F | F | a | a | v | v | ||
G | G | b | b | w | w | ||
H | H | c | c | x | x | ||
I | I | d | d | y | y | ||
J | J | e | e | z | z | ||
K | K | f | f |
fontello
Character | Icon |
---|---|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|

|
|
Pop Up Boxes
With the additional support for <a>
links and IDs using , Roll20 has also given an example of how this functionality can be used to create pop up content using the :target selector.
See Also
- Building Character Sheets
- Designing Character Sheet Layout - tips on how to best design the broad strokes of a character sheet
- Image use in character sheets - How to include images on your character sheets
- Sheet Design Best Practices
- Sheetworkers - relevant for many of the more complicated tricks on the page
- Sheet Author Tips - misc. best practices and good ideas for those who often work with sheets
- Sheet Sandbox – the better editor to use when you code your character sheets
- Roll20 GitHub repository
- List of all pages related to "Character Sheet Creation"
Guides
- Introduction to HTML - MDN web docs
- Introduction to CSS - MDN web docs
- Introduction to JavaScript - MDN web docs