← Examples · Ship Pack · How this page works

CSS-only spin

No JavaScript on this page. Each ship is an element whose background is its sheet-alpha.png; two steps(6) animations walk the 6 × 6 grid, and the radio buttons below hold one heading with :has() and CSS mod(). Hover a ship to pause it.

Heading

Ships from the Yodacon Ship Pack by Paul Richeson (ConEx, 1997), GPL-2.0-or-later — github.com/yodacon/yodacon-ship-pack

Yodacon Ship Pack · Tutorial Note 9 · October 2026

Spinning Without Script: How the CSS-Only Example Uses the Pack

Abstract—The CSS-only example animates pack ships with no JavaScript: two steps(6) animations, one per background axis, walk the 6 × 6 sheet in frame order, and mod(), round() and :has() hold any heading chosen by a radio button. This note derives the frame sequence and follows the page's stylesheet.

Index Terms—CSS animation, steps(), CSS math functions, :has(), sprite sheets.

I. Introduction

The CSS example has no script at all. Eleven ships spin in a hangar, and a row of radio buttons stops them all at one heading. Everything is in the page's stylesheet, examples/css/index.html, and in one style attribute per ship. It is the form to use where JavaScript is unwelcome: an e-mail-safe page, a README preview, a loading screen.

II. Assets Used

Table I. What the page takes from the pack
AssetWhereUsed for
sheet-alpha.png ×11index.html:104Each ship's background
ship.json (copied)index.html:104Frame size, as --w and --h

With no script the page cannot read ship.json, so each ship's frame size is copied into its style attribute with the sheet URL and a whole-number scale --s (--sheet, --w, --h, --s). Those four properties are the only per-ship data; the rest of the page is shared rules.

III. The Sprite

As in the DOM example [2], an element one frame in size shows the sheet, scaled by s, as its background (Listing 1). Two derived properties, --cell-w and --cell-h, hold the scaled frame size so the later rules can stay short.

Listing 1. The sprite rule, index.html:27–34
27.ship {28  --cell-w: calc(var(--w) * var(--s) * 1px);29  --cell-h: calc(var(--h) * var(--s) * 1px);30  display: block;31  width: var(--cell-w);32  height: var(--cell-h);33  background: var(--sheet) no-repeat 0 0 / calc(var(--cell-w) * 6) auto;34  image-rendering: pixelated;      /* keep the 1997 pixels square */

IV. Spinning With Two Step Animations

The frames run 0 to 35 across six rows of six. A single steps(36) animation would need the frames in one strip; the pack's sheets are a grid, so the page runs two animations at once (Listing 2). One moves background-position-x through six columns in T/6, repeating; the other moves background-position-y through six rows in T. At time t the column is ⌊36t/T⌋ mod 6 and the row ⌊6t/T⌋, so the frame shown is

f = 6 · row + col = ⌊36 t / T⌋ mod 36(1)

which is the clockwise spin, one frame every T/36 = 0.1 s. The two properties are animated separately, so neither animation disturbs the other. Hovering pauses both with animation-play-state, and prefers-reduced-motion turns them off (index.html:68).

Listing 2. The spin, index.html:40–46
40  --turn: 3.6s;                    /* one full turn; 0.1 s a frame */41  animation: cols calc(var(--turn) / 6) steps(6) infinite,42             rows var(--turn) steps(6) infinite;43}44@keyframes cols { to { background-position-x: calc(var(--cell-w) * -6); } }45@keyframes rows { to { background-position-y: calc(var(--cell-h) * -6); } }46.ship:hover { animation-play-state: paused; }

V. Holding a Heading

A fixed frame f needs its column and row, and CSS can now compute both: mod() and round(down, …) (Listing 3). The heading itself comes from the radio buttons: for each one a rule body:has(#h9:checked) .ship { --f: 9; } sets --f on every ship, so the form state reaches the ships without a line of script. Choosing “Spin” removes the override and the animations resume.

Listing 3. Frame f in CSS, index.html:49–55
49body:not(:has(#spin:checked)) .ship {50  animation: none;51  background-position:52    calc(mod(var(--f), 6) * var(--cell-w) * -1)53    calc(round(down, var(--f) / 6) * var(--cell-h) * -1);54}55body:has(#h0:checked) .ship { --f: 0; }

VI. Using It in Your Project

  1. Copy the .ship rules and the two @keyframes.
  2. For each ship, write its sheet URL and its frame_width and frame_height from ship.json into a style attribute.
  3. Set --turn for speed, or --f (0–35) with animation: none for a fixed heading.

Browsers without mod() and round() still spin; only the held heading needs them.

VII. Conclusion

Two steps(6) animations on the two background axes reproduce the pack's frame order exactly, and CSS math picks any single frame, so a pack ship can spin or hold a heading with no JavaScript.

References

  1. Yodacon Ship Pack, “From sprite sheet to moving ship,” Tutorial Note 1, examples/, 2026. examples/#tutorial
  2. Yodacon Ship Pack, “A ship in plain HTML and CSS,” Tutorial Note 2, examples/dom/.
  3. W3C, CSS Values and Units Module Level 4, §10 “Stepped value functions,” Working Draft, 2024.
  4. W3C, CSS Easing Functions Level 1, §3 “Step easing functions,” Candidate Recommendation, 2023.