Visual novels are one of the most popular storytelling formats in interactive fiction. With their full-screen backgrounds, character sprites, and dialogue boxes, they create an immersive reading experience that draws players into your story. The good news? You can achieve this polished look in Arcweave's Play Mode using custom CSS.
In this tutorial, we'll show you how to customize your Play Mode interface to look like a professional visual novel—no coding experience required!
By the end of this guide, you'll know how to:
To follow this tutorial, you will need:
Before diving into CSS, structure your project like this:
Now, let's get started!
First, let's open the CSS editor where all the magic happens:
The Style Editor shows you the current CSS and lets you write custom styles that will apply to your entire Play Mode.
A visual novel interface has three main visual parts that we'll customize:
Let's tackle them one by one:
By default, element covers appear as small images. Let's make them fill the entire screen:
/* Full-screen background container */
.prototype__media {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
/* Make the image cover the screen */
.prototype__media img.single {
width: 100%;
height: 100%;
object-fit: cover;
}
What this does:
position: absolute lets us place the image anywherewidth: 100% and height: 100% make it fill the screenobject-fit: cover ensures the image fills the space without distortionz-index: 1 puts it in the background, behind characters and dialogueCharacters should appear at the bottom of the screen, standing above the dialogue box:
/* Character container */
.prototype__components {
position: absolute;
bottom: 350px;
left: 0;
right: 0;
height: 60vh;
display: flex;
align-items: flex-end;
z-index: 2;
padding: 0 5%;
}
/* Character sprite styling */
.prototype__components .comp {
height: 100%;
background: none;
object-fit: contain;
object-position: bottom;
filter: drop-shadow(0 0 20px rgba(0, 0, 0, 0.5));
}
What this does:
bottom: 350px positions characters above the dialogue boxheight: 60vh makes characters take up 60% of screen heightalign-items: flex-end aligns characters to the bottomobject-position: bottom keeps feet anchored at the bottomfilter: drop-shadow() adds a subtle shadow for depth💡 Customization tip: Adjust bottom: 350px to change how high characters appear. Try 400px for more space or 300px for less.
The dialogue box is where your text appears. This is important: we need to make sure long text can scroll!
/* Dialogue box container */
.prototype__body {
position: absolute;
bottom: 50px;
left: 50px;
right: 50px;
height: 300px;
background: linear-gradient(135deg,
rgba(30, 41, 59, 0.85) 0%,
rgba(15, 23, 42, 0.98) 100%);
border: 2px solid #475569;
border-radius: 15px;
z-index: 3;
padding: 25px 60px;
backdrop-filter: blur(10px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
display: flex;
}
/* Text container with scroll functionality */
.prototype__text {
overflow: auto;
width: 100%;
scrollbar-width: thin;
scrollbar-color: #475569 transparent;
}
/* Custom scrollbar for Chrome/Safari */
.prototype__text::-webkit-scrollbar {
width: 6px;
}
.prototype__text::-webkit-scrollbar-track {
background: transparent;
}
.prototype__text::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
/* Text styling inside the box */
.prototype__text .editor .ProseMirror {
color: #ffffff;
font-size: 30px;
line-height: 1.7;
font-family: 'Segoe UI', 'Noto Sans', sans-serif;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
}
What this does:
bottom: 50px positions it near the screen bottombackground: linear-gradient() creates a semi-transparent gradientbackdrop-filter: blur(10px) blurs the background behind the boxborder-radius: 15px rounds the cornersoverflow: auto - This is crucial! It enables scrolling when text is longscrollbar-width: thin and ::-webkit-scrollbar style the scrollbar to match your themetext-shadow makes text more readable⚠️ Important: Without the overflow: auto property, long dialogue will be cut off! This ensures players can always read everything.
Player choices should appear above the dialogue box:
/* Options container */
.prototype__options {
position: absolute;
bottom: 360px;
left: 50%;
transform: translateX(-50%);
display: block;
z-index: 4;
width: 90%;
max-width: 800px;
}
/* Individual choice button */
.prototype__option {
background: linear-gradient(135deg,
rgba(51, 65, 85, 0.95) 0%,
rgba(30, 41, 59, 0.98) 100%);
border: 2px solid #60a5fa;
border-radius: 50px;
padding: 20px 35px;
margin: 0 0 30px;
color: #ffffff;
font-size: 24px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3);
}
/* Hover effect */
.prototype__option:hover {
transform: translateY(-4px) scale(1.05);
border-color: #a78bfa;
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4);
}
What this does:
bottom: 360px places buttons just above the dialogue boxtransform: translateX(-50%) centers them horizontallyborder-radius: 50px creates pill-shaped buttonstransition: all 0.3s smoothly animates hover effects:hover makes buttons lift up when you mouse over themHere's a pro technique: define your colors once at the top, then reuse them throughout:
:root {
/* Dialogue box theme */
--vn-dialogue-bg: linear-gradient(135deg,
rgba(30, 41, 59, 0.85) 0%,
rgba(15, 23, 42, 0.98) 100%);
--vn-dialogue-border: #475569;
--vn-dialogue-text: #ffffff;
/* Button theme */
--vn-option-bg: linear-gradient(135deg,
rgba(51, 65, 85, 0.95) 0%,
rgba(30, 41, 59, 0.98) 100%);
--vn-option-border: #60a5fa;
--vn-option-text: #ffffff;
/* Text sizes */
--vn-font-size-dialogue: 30px;
--vn-font-size-option: 24px;
}
Then use them like this:
.prototype__body {
background: var(--vn-dialogue-bg);
border-color: var(--vn-dialogue-border);
}
Why this is useful: Change one value at the top and it updates everywhere! Perfect for trying different color schemes.
Don't forget about mobile players! Add these media queries:
@media (max-width: 1000px) {
/* Adjust character positioning */
.prototype__components {
bottom: 150px;
height: 50vh;
}
/* Smaller dialogue box */
.prototype__body {
bottom: 20px;
left: 20px;
right: 20px;
height: 150px;
padding: 15px 30px;
}
/* Smaller text */
.prototype__text .editor .ProseMirror {
font-size: 20px;
}
/* Adjust options */
.prototype__options {
bottom: 180px;
}
.prototype__option {
font-size: 18px;
padding: 12px 30px;
}
}
Here's where things get really interesting! You might want to emphasize certain words—like a character's name in a different color, important clues in bold and red, or sound effects in a stylized font.
The Challenge: Arcweave doesn't let you inject custom HTML classes directly into your text.
The Solution: We can "hijack" the formatting tools that Arcweave already provides!
When you format text in Arcweave's editor (italic, bold, underline), it generates standard HTML tags:
<em> tags<strong> tags<u> tagsWe can target these tags with CSS and override their default appearance. The key is to use the element ID to make sure we only affect specific elements, not every italic word in your entire story.
There are two easy ways to find an element's ID:
Method 1: Using the debugger (recommended)
el-cb35db92-3c07-41bf-9c45-580ad6b891b4)Method 2: From the Editor
1. Format text in the editor
Go to your element and edit it:
2. Write the CSS
Now in the Style Editor, target that specific formatted text:
/* Target italic text in a specific element and make it red */
#el-your-element-id ditor .editor-content em {
color: red;
font-style: normal; /* Remove the default italic effect */
}
Make character names appear in their signature color:
In Arcweave: Format "Alice" with italic CSS:
#el-your-element-id .prototype__text .editor .editor-content em {
color: #60a5fa; /* Blue for Alice */
font-style: normal;
font-weight: bold;
}
Highlight clues that players should remember:
In Arcweave: Format clue words with bold CSS:
#el-your-element-id .prototype__text .editor .editor-content strong {
color: #fbbf24; /* Gold color */
font-weight: 900;
text-shadow: 0 0 10px rgba(251, 191, 36, 0.5); /* Glow effect */
}
Create stylized sound effects:
In Arcweave: Format "crash" with underline CSS:
#el-your-element-id .prototype__text .editor .editor-content u {
text-decoration: none; /* Remove underline */
color: #ef4444;
font-family: 'Impact', sans-serif;
font-size: 1.2em;
font-style: italic;
letter-spacing: 2px;
}
You can also style entire paragraphs using nth-child:
/* Style the first paragraph differently (maybe it's narration) */
#el-your-element-id .prototype__text .editor .ProseMirror p:first-child {
color: #a78bfa;
font-style: italic;
font-size: 0.9em;
}
/* Style the second paragraph (maybe it's a character speaking) */
#el-your-element-id .prototype__text .editor .ProseMirror p:nth-child(2) {
color: #60a5fa;
font-weight: bold;
}
Here are some creative ways to use this technique:
1. Dialogue with different character voices:
/* Character 1 - uses italic */
#el-your-element-id em {
color: #60a5fa;
font-style: normal;
font-weight: 600;
}
/* Character 2 - uses bold */
#el-your-element-id strong {
color: #f472b6;
font-weight: 700;
}
/* Narrator - uses underline */
#el-your-element-id u {
text-decoration: none;
color: #a78bfa;
font-style: italic;
}
2. Thoughts vs. spoken dialogue:
/* Inner thoughts in italic */
#el-your-element-id em {
color: #94a3b8;
font-style: italic;
opacity: 0.8;
}
3. Branching indicators:
/* Highlight choices that lead to different paths */
#el-your-element-id strong {
color: #fbbf24;
font-weight: bold;
text-decoration: underline;
}
⚠️ Always include the Element ID! If you forget it, you'll accidentally style EVERY italic/bold/underline word in your entire project:
/* ❌ DON'T DO THIS - affects everything */
em {
color: red;
}
/* ✅ DO THIS - affects only one element */
#el-your-element-id em {
color: red;
}
⚠️ Limitations: You can only use three "channels" (italic, bold, underline) per element. If you need more variety, create different elements or use paragraph targeting.
⚠️ Testing: Always test in Play Mode to make sure your styles look right!
Here's the full CSS ready to copy and paste.
/* ========== FULL-SCREEN BACKGROUND ========== */
.prototype__media {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.prototype__media img.single {
width: 100%;
height: 100%;
object-fit: cover;
}
/* ========== CHARACTER SPRITES ========== */
.prototype__components {
position: absolute;
bottom: 350px;
left: 0;
right: 0;
height: 60vh;
display: flex;
align-items: flex-end;
z-index: 2;
padding: 0 5%;
}
.prototype__components .comp {
height: 100%;
background: none;
object-fit: contain;
object-position: bottom;
filter: drop-shadow(0 0 20px rgba(0, 0, 0, 0.5));
}
/* ========== DIALOGUE BOX ========== */
.prototype__body {
position: absolute;
bottom: 50px;
left: 50px;
right: 50px;
height: 300px;
background: linear-gradient(135deg,
rgba(30, 41, 59, 0.85) 0%,
rgba(15, 23, 42, 0.98) 100%);
border: 2px solid #475569;
border-radius: 15px;
z-index: 3;
padding: 25px 60px;
backdrop-filter: blur(10px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
display: flex;
}
/* Text container with scrolling */
.prototype__text {
overflow: auto;
width: 100%;
scrollbar-width: thin;
scrollbar-color: #475569 transparent;
}
/* Custom scrollbar for Chrome/Safari */
.prototype__text::-webkit-scrollbar {
width: 6px;
}
.prototype__text::-webkit-scrollbar-track {
background: transparent;
}
.prototype__text::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 3px;
}
/* Text styling */
.prototype__text .editor .ProseMirror {
color: #ffffff;
font-size: 30px;
line-height: 1.7;
font-family: 'Segoe UI', 'Noto Sans', sans-serif;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
}
/* ========== CHOICE BUTTONS ========== */
.prototype__options {
position: absolute;
bottom: 360px;
left: 50%;
transform: translateX(-50%);
display: block;
z-index: 4;
width: 90%;
max-width: 800px;
}
.prototype__option {
background: linear-gradient(135deg,
rgba(51, 65, 85, 0.95) 0%,
rgba(30, 41, 59, 0.98) 100%);
border: 2px solid #60a5fa;
border-radius: 50px;
padding: 20px 35px;
margin: 0 0 30px;
color: #ffffff;
font-size: 24px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 20px rgba(59, 130, 246, 0.3);
}
.prototype__option:hover {
transform: translateY(-4px) scale(1.05);
border-color: #a78bfa;
box-shadow: 0 8px 30px rgba(99, 102, 241, 0.4);
}
/* ========== STYLING SPECIFIC WORDS (EXAMPLE) ========== */
/* Replace with your actual element ID */
#el-your-element-id .prototype__text .editor .editor-content em {
color: #60a5fa;
font-style: normal;
font-weight: bold;
}
/* ========== MOBILE RESPONSIVE ========== */
@media (max-width: 1000px) {
.prototype__components {
bottom: 150px;
height: 50vh;
}
.prototype__body {
bottom: 20px;
left: 20px;
right: 20px;
height: 150px;
padding: 15px 30px;
}
.prototype__text .editor .ProseMirror {
font-size: 20px;
}
.prototype__options {
bottom: 180px;
}
.prototype__option {
font-size: 18px;
padding: 12px 30px;
}
}
Want to tweak the look? Here are the most common adjustments:
| What to Change | Where to Look | Example Values |
|---|---|---|
| Character height | bottom: 350px in .prototype__components |
300px - 500px |
| Character size | height: 60vh in .prototype__components |
40vh - 80vh |
| Dialogue box position | bottom: 50px in .prototype__body |
30px - 100px |
| Dialogue box height | height: 300px in .prototype__body |
200px - 400px |
| Text size | font-size: 30px in .ProseMirror |
24px - 36px |
| Scrollbar color | scrollbar-color in .prototype__text |
Any hex color |
| Button colors | border: 2px solid #60a5fa in .prototype__option |
Any hex color |
| Button shape | border-radius: 50px in .prototype__option |
0px (square) - 50px (pill) |
Create different moods by changing the gradient colors:
Dark & Mysterious:
background: linear-gradient(135deg,
rgba(15, 15, 25, 0.9) 0%,
rgba(5, 5, 15, 0.95) 100%);
Light & Airy:
background: linear-gradient(135deg,
rgba(240, 240, 250, 0.85) 0%,
rgba(220, 220, 240, 0.95) 100%);
Want a different font? Add this at the top:
@import url('<https://fonts.googleapis.com/css2?family=Your+Font+Name&display=swap>');
.prototype__text .editor .ProseMirror {
font-family: 'Your Font Name', sans-serif;
}
If you have multiple character components attached to an element, they'll automatically arrange side-by-side thanks to display: flex in .prototype__components.
Match your scrollbar to your theme by changing the color:
.prototype__text {
scrollbar-color: #your-color transparent;
}
.prototype__text::-webkit-scrollbar-thumb {
background: #your-color;
}
To test if your scrolling works, write a long element with lots of text. The scrollbar should appear automatically when content exceeds the dialogue box height.
Solution: Increase height: 60vh to 70vh or 80vh
Solution: Increase bottom: 350px in .prototype__components
Solution: Increase the opacity in your background gradient (change 0.85 to 0.95)
Solution: Increase bottom: 360px in .prototype__options
Solution: Check your media query values and adjust the breakpoint (max-width: 1000px)
Solution: Make sure you have overflow: auto on .prototype__text
Solution: Check that scrollbar-width: thin and the webkit scrollbar styles are present
Solution: Make sure you're including the specific element ID: #el-your-id em { } not just em { }
Now that you have a beautiful visual novel interface, here are some ideas to take it further:
transition and transform properties for fade-insCongratulations! You've just transformed your Arcweave project into a professional-looking visual novel. The best part? All of this styling happens in Play Mode, so you can iterate quickly without touching your project structure.
You've learned:
Remember: CSS is all about experimentation. Don't be afraid to adjust values, try different colors, or completely redesign elements. The Style Editor updates in real-time, so you can see your changes immediately.
Want to share your creation? Use Arcweave's Share feature to make your project public and let others experience your visual novel!
Need more help? Join the Arcweave Discord community where you can share your CSS creations and get feedback from other creators.
Happy storytelling! ✨
Have you created something cool with these CSS techniques? We'd love to see it! Share your projects with us on Discord or tag us on social media.