Actions

Work Header

Rating:
Archive Warning:
Fandom:
Additional Tags:
Language:
English
Stats:
Published:
2026-08-27
Words:
575
Chapters:
1/1
Comments:
5
Kudos:
14
Bookmarks:
11
Hits:
191

AO3 Radio Buttons

Summary:

Make true radio buttons on AO3, that can be selected with just one click.

Notes:

I made this because I couldn't find a real way to make radio buttons with details. Disclaimer that this makes use of a specific script in AO3's codebase. So if the devs decide to change it, this might break. But the code has stayed the same for about 15 years. Is it too much to hope it will stay that way forever? lmao

(See the end of the work for more notes.)

Work Text:


What is your favourite colour?

I also like !


What is your least favourite colour?

Yuck, !


Unlike with using details, this method only requires one click to "select" a particular value and "deselect" the rest. Unlike with using :target, this allows for multiple indepedent sets of buttons.

How it works

This method utilises the .expand and .shuffle CSS class, whose on-click behaviours are defined in application.js.

On click, an .expand element will have the inline HTML style style="display: none;". The sibling .shuffle elements after it will then have their inline style be set to display: block; or whatever their initial display value is.

A radio-group consists of a set of radio buttons with the classes .expand.shuffle. When one button is clicked, it will have style="display: none;", and all the buttons after it will not. Therefore, the user selected button is the last occuring element with .expand[style*="none"]. We can select this element with :nth-last-child(1 of .expand[style*="none"]).

Accessibility

To ensure that the radio buttons can be selected using keyboard, use <a> tags for the buttons and set the href value to a non-existent target. This is to prevent navigating away from the button.

Alternatively, if you are using :target for something else, add the .check_none class to the radio buttons. Also seen in application.js, this class will set the link to have event.preventDefault().

Unexpected behaviour on Chromium Browsers


#workskin .my-form:has(.radio-group > :nth-child(1):nth-last-child(1 of .expand[style*="none"])) .output::before {
  content: "Red";
  background: red;
  color: white;
}

During testing, I encountered an issue on Edge and Chrome where the above selector would not act as intended, unless I made changes to the code and "forced" the browsers to re-evaluate the CSS. Therefore .output would not respond to inline style attribute changes.

I was not able to pinpoint the exact cause of this, but it appears to be a known issue with Chromium browsers, :has(), and complicated statements with attribute selectors. Firefox does not seem to have this issue.

The fix is to add a "dummy" CSS declaration with :has and an inner [style] selector.


/* chromium bug fix */
#workskin .radio-group:has([style*="none"]) {
  display: block;
}

CSS


/* appearance for radio buttons */

#workskin .radio-group .expand,
#workskin .radio-group .expand:visited,
#workskin .radio-group .expand:hover,
#workskin .radio-group .expand:active {
  display: block !important; /* ignore html inline styling */
  cursor: pointer;
  color: currentColor;
  border: none;
  outline: none;
}

#workskin .radio-group .expand:hover::before {
  filter: brightness(.7);
}

/* keyboard navigation focus */
#workskin .radio-group .expand:focus-visible {
  outline: 1px dotted currentColor;
}

#workskin .radio-group .expand::before {
  display: inline-block;
  content: "";
  width: .7em;
  aspect-ratio: 1;
  margin-right: .5em;
  border: 1px solid currentColor;
  border-radius: 50%;
  box-sizing: border-box;
}

/* appearance for selected radio buttons */
#workskin .radio-group :nth-last-child(1 of .expand[style*="none"])::before {
  border: 2px solid transparent;
  box-shadow: 0 0 0 1px dodgerblue, inset 0 0 0 1em dodgerblue;
}



/* output results */

#workskin .output::before {
  content: "colours";
  color: currentcolor;
}

#workskin .my-form:has(.radio-group > :nth-child(1):nth-last-child(1 of .expand[style*="none"])) .output::before {
  content: "Red";
  background: red;
  color: white;
}

#workskin .my-form:has(.radio-group > :nth-child(2):nth-last-child(1 of .expand[style*="none"])) .output::before {
  content: "Blue";
  background: blue;
  color: white;
}

#workskin .my-form:has(.radio-group > :nth-child(3):nth-last-child(1 of .expand[style*="none"])) .output::before {
  content: "Green";
  background: green;
  color: white;
}

#workskin .my-form:has(.radio-group > :nth-child(4):nth-last-child(1 of .expand[style*="none"])) .output::before {
  content: "Yellow";
  background: yellow;
  color: black;
}

/* chromium bug fix */
#workskin .radio-group:has([style*="none"]) {
  display: block;
}

Notes:

imo this has huge potential for html tryhard stuff. since it provides an alternative to details, which can only switch between 2 values on one click. while a radio group can toggle between as many values as you want! but again it's a use at your own risk thing 😭😭😭