Post

CSS Zen Garden on itch.io: Styling Jams Page with pure CSS

CSS Zen Garden on itch.io: Styling Jams Page with pure CSS

While preparing the 20 Second Game Jam 2025 page, I was reminded of the classic CSS Zen Garden, which was (is?) a beautiful demonstration of what you can achieve with CSS alone when you can’t touch the HTML. On itch.io, you can customize your jam page with custom CSS, but you’re working with a fixed HTML structure that the platform generates. No JavaScript allowed, no HTML modifications possible (except for the content, with it’s own limitations). Just pure CSS wizardry.

We go through this every year, and every year we have to relearn the same lessons. So for posterity, here are some of the creative (and sometimes ridiculous) CSS hacks I used to achieve the design I wanted this year.

Shifting the Logo Into the Header

The Problem: I wanted to have the logo overlap with the header. Of course, that also meant moving some of the components in the header around to make space around the logo.

The Solution: This was achieved not by moving the logo down, which would’ve left a large empty space at the top of the page, but rather by shifting the body content up using a negative margin-top. I also hid the jam name, since it’s in the logo, and adjusted the display and text-align properties of some of the content.

1
2
3
4
5
6
7
8
9
10
11
.view_jam_page .jam_body {
  border-top-left-radius: 40px;
  border-top-right-radius: 20px;
  margin-top: -135px;
}
.view_jam_page .jam_header_widget .header_columns { display: block; }
.view_jam_page .jam_header_widget .jam_title_header { display: none; }
.view_jam_page .jam_header_widget .jam_host_header {
  text-align: right;
  ...
}

There’s more to this story below, caused by Akz helpfully adding a hashtag for the jam.

The Safari Gradient Bug

The Problem: Xeno has a rediculously wide screen and we chose a kinda bright background color.

Dusty pink doesn’t sound all that bright, but when you cover most of a 39” ultra wide monitor with it, it can get quite intense. We went through several iterations, but in the end I decided that we should use the olive green as vertical green bars on the sides using viewport-based calculations:

1
2
3
4
5
6
7
8
9
10
11
12
body {
  background: linear-gradient(
    to right,
    #4C5424 calc(50vw - 680px - 400px),
    #4C5424 calc(50vw - 680px),
    #FF8E71 calc(50vw - 680px),
    #FF8E71 50vw,
    #FF8E71 calc(50vw + 680px),
    #4C5424 calc(50vw + 680px),
    #4C5424 calc(50vw + 680px + 400px)
  );
}

This worked great in Chrome and Firefox, but Safari has a known bug (not known to me at the time, of course) where gradients with calc() and viewport units render incorrectly when zooming. This was discovered by Akz, who does not have 39” available and apparently likes to zoom in on things. The gradient would glitch out, bars would shift, and it looked terrible.

The Solution: Browser detection via CSS feature queries. We give Safari users a solid background color while everyone else gets the gradient:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Default: gradient for Chrome, Firefox, etc. */
body {
  background: linear-gradient(
    to right,
    #4C5424 calc(50vw - 680px - 400px),
    ...
  );
}

/* Safari gets solid background only */
@supports (-webkit-hyphens:none) {
  body {
    background-color: #FF8E71 !important;
  }
}

The -webkit-hyphens:none is a Safari-specific CSS property that we can detect. While it’s not ideal to have different experiences across browsers, it’s better than a broken experience. And besides, with that both Xeno and Akz are happy.

The Invisible Bullet Trick

The Problem: Akz added a hashtag for the jam. Great idea, but it caused the host header to overlap with the logo I carefully moved down.

The jam host header itch.io now generates looks like this:

1
Hosted by Akzidenz, Marc, xenobrain · #20SecondGameJam

To avoid the overlap, I wanted the hashtag on its own line without that bullet ( · ), but we can’t modify the HTML. The bullet is just a text node between two links, and CSS can’t target text nodes directly.

I started by making the hashtag link display: block to force it to wrap:

1
2
3
.jam_host_header a[href*="hashtag"] {
  display: block;
}

This wrapped the hashtag to a new line, but the bullet was still visible on the previous line. I tried various approaches: font-size: 0 (hid the commas between host names too), negative margins (messy and unreliable), and word-spacing hacks (didn’t work consistently).

The Solution: This might be the evilest CSS hack I’ve ever come up with: a gradient clipped to text. I applied a horizontal gradient that fades from the text color to transparent in the last 2em, then use background-clip: text to apply it only to the text:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.jam_host_header {
  text-align: right;
  background: linear-gradient(
    to right,
    #333 0%,
    #333 calc(100% - 2em),
    transparent 100%
  );
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

.jam_host_header a {
  color: #4c5424;
  -webkit-text-fill-color: #4c5424;
}

The gradient makes all text transparent at the parent level, then we override the links with their proper color. The bullet at the end fades to transparent while the host names and commas remain visible. A bit of margin tweaking to line things up, and it looks great.

The Problem: itch.io’s HTML sanitizer is aggressive (I guess for security). It strips id attributes and removes href attributes from relative anchor links. So this doesn’t work:

1
2
<h2 id="faq">FAQ</h2>
<a href="#faq">See FAQ</a>

Both get stripped, leaving you with non-functional links.

The Solution: Old-school <a name=""> tags and full URLs:

1
2
<h2><a name="faq"></a>FAQ</h2>
<a href="https://itch.io/jam/20-second-game-jam-2025#faq">See FAQ</a>

The <a name=""> syntax (deprecated in modern HTML but still supported) creates the anchor point, and using the full URL with hash fragment preserves the link. Not elegant, but thanks to browsers’ incredible work at maintaining backwards compatibility, it works!

Unfortunately you cannot test these in your unpublished jam page, because that’s on a different url, so make sure to test them as soon as you publish.

Custom CSS for your Jam Content

The Problem: You can switch to HTML mode in the Itch jam content editor but as mentioned above, the HTML sanitizer is very aggressive. Custom classnames are stipped out by default.

The Solution: Well, custom classnames are stipped out by default unless they start with custom-. This is well known, and documented, but we still find ourselves having to rediscover it every year.

Conclusion

There are game jam hosts and indie developers that have abused Itch-hosted pages far more creatively than what I have done here, but at least next year I can come back to this post and remind myself of the basics and some clever – for some definition of the word “clever” – hacks that worked this year.

If you want to see all these hacks in action, check out the 20 Second Game Jam 2025 page and poke around with the developer tools in your browser. I will write a separate post describing how I worked around the maddening flow of editing the content HTML on one page, and the CSS in a tiny text box on a separate page.

This post is licensed under CC BY 4.0 by the author.