Advertisement

CSS :has() Character Select Screen

| by Vladimir | 3 min read | code by Sicontis
Beginner

Tech & Dependencies

HTML CSS JavaScript
Vue

Features

  • CSS State Management
  • No JavaScript Logic
  • Responsive Grid

Browser Support

Chrome 105+ Edge 105+ Firefox 121+ Safari 15.4+

Core

This is a CSS :has() Character Select Screen. It replicates the classic user interface of a fighting game’s selection menu, using modern CSS to handle all state logic. Its function is to demonstrate how the :has() pseudo-class can completely replace JavaScript for managing visibility in a selection-based UI, triggering changes in the main content area based on the state of a radio button group.

Specs

  • Weight: ~75 KB (Vue dependency for templating). The core logic is pure CSS.
  • Performance: Exceptionally high. Because the state is managed directly by the CSS engine, there is zero latency between selecting a character and the main image updating.
  • Theming / Customization: Easily customizable by updating the characters array in the Vue data object.
  • Responsiveness: Uses a CSS Grid layout with media queries to switch from an 8-column to a 4-column layout for the character portraits on smaller screens.
  • Graceful Degradation: [!] Relies entirely on the :has() pseudo-class. In older browsers, all large character images will be hidden, and the selection will not work.

Anatomy

The component uses hidden radio inputs as a state controller for the main display area.

  • HTML (The Skeleton): The Vue template generates the structure. An <aside> contains a list of <li> elements, each with a hidden <input type="radio"> and a <label> (the character portrait). A separate <section> contains all the large character <figure> elements.
  • CSS (The Skin): Uses SCSS for styling. The core logic is handled by CSS, not JavaScript. mix-blend-mode: multiply on the main image creates a rich color interaction with the background texture.
  • JS (The Nervous System): Vue.js is used only as a templating engine to generate the repetitive HTML. There is no JavaScript logic for handling clicks, state, or visibility.

Logic

The entire interaction is powered by the “Parent State Selection” logic of :has():

/* Hide all main images by default when any radio is checked */
aside:has(:checked) + section figure {
  display: none;
}

/* Then, only show the one that matches the checked ID */
aside:has(#Ryu:checked) + section figure[data-name="Ryu"] {
  display: block;
}

This is a two-step CSS declaration that is incredibly efficient. First, it says, “If the <aside> element has any checked radio button inside it, then select the adjacent <section> and hide all <figure> elements within it.” Immediately after, a more specific rule says, “If the <aside> element has the specific radio button with the ID #Ryu checked, then find the adjacent <section> and display the one <figure> that has the matching data-name='Ryu'.

Feel

Instant, snappy, and retro. The interaction feels like a native application or a classic arcade console menu. The hover effect, where unselected portraits blur and desaturate (filter: grayscale(1) blur(0.05em)), expertly guides the user’s focus to their current selection without any distracting animations.

Advertisement