2

I'd like to contribute an interactive SVG graph to Wikisource. There are quite strict requirements on the format, as indicated in the title:

  • SVG can contain inline style attributes and <style> elements, but besides that:
  • no SMIL,
  • no JavaScript,
  • no HTML foreignObject isles,
  • so no HTML <input> and CSS :checked tricks.

I know about the :root:has(input[…]:checked) something {…} toggling technique, as seen in this HTML sample:

#main:has(#chkbox1)          #triangle  { display:none; }
#main:has(#chkbox1:checked)  #triangle  { display:block; }

#main:has(#chkbox2)          #square    { display:none; }
#main:has(#chkbox2:checked)  #square    { display:block; }

#main:has(#chkbox3)          #circle    { display:none; }
#main:has(#chkbox3:checked)  #circle    { display:block; }
<div id="main">
  <div id="triangle">Triangle</div>
  <div id="square">Square</div>
  <div id="circle">Circle</div>

  <input type="checkbox" id="chkbox1" checked="true">
  <input type="checkbox" id="chkbox2" checked="true">
  <input type="checkbox" id="chkbox3" checked="true">
</div>

(As you can see, this works without using JavaScript.) But it is just a HTML, that does not fit the requirements.

How can I simulate same functionality (hiding/showing some elements) without using any <input> tags and without using JavaScript?

I aim for a solution in which each <input> is replaced by some SVG node that simulates a checkbox using only allowed techniques, i.e., presumably CSS.

Here is an image of SVG generated on CodePen with the SVG I'd like to contribute, including the interactivity: https://codepen.io/schlebe/pen/gbbOrYK

Complex graph with several colourful function progressions, each toggled with a corresponding checkbox located on the right side.

16
  • 2
    Why not use developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/…? What are the actual goals and constraints here? Commented Apr 17 at 9:43
  • 1
    Yeah, so is your actual question "how do I make an interactive SVG that allows showing/hiding certain elements when others are clicked, without using JavaScript"? Commented Apr 17 at 9:56
  • 2
    Right, but does the target platform accept SVGs with <foreignObject> at all? In my experience, that's the no-no... Commented Apr 17 at 10:51
  • 1
    @schlebe maybe state your motivation first and try to find precise restrictions for user-contributed SVGs for Wikimedia: 1. is foreignObject OK?, 2. is SMIL OK? 3. is JS OK? 4. is <style> inside SVG OK? (I did a quick research and did not get clear results, sadly). To me your goal is not entirely clear either, to be honest. And there is a chance than even these possible solutions will turn out as false, so don't hold a high hopes. Commented Apr 17 at 19:20
  • 1
    @schlebe Ok, so if <style> is allowed, then it should be hackable, provided the contents of the style is not sanitised for weird stuff. For a plain HTML (what you actually do NOT need) the answer could look like this: codepen.io/myf/pen/QwwwNdo , right? Commented Apr 17 at 19:44

3 Answers 3

6
+500

Kaiido's ":target-based" answer is great, mainly because it preserves accessibility to a large extent (even keyboard!), and has neat ability to address any state using URL#hash so is indeed preferable.

Adding this alternative approach that is comparatively simpler, since introducing additional control here requires linear or (even constant, see below), not exponential additions.

Flipping "display" with :active and spring-loaded animation

It exploits super-short paused animations and :active trigger that releases the animation with animation-play-state: running. Animation runs and the state is kept thanks to the animation-fill-mode: both. For returning back to the original state, animation: none is used, what after release basically resets the animation timeline back to start, when the original animation is re-applied.

As extra challenge, this sample does not even use :has(), so it's CSS is pretty old-school. Also for simplicity, the "Trigger" elements are just on/of (filled/empty) rectangles superimposed over each other, not mutually toggled.

<embed height="150" src='data:image/svg+xml,<svg
 xmlns="http://www.w3.org/2000/svg"
 viewBox="0 0 100 50"
 fill="none"
 pointer-events="all"
><style>
  /* Using scale-to-zero for hiding, see notes */
  @keyframes hide { to { transform: scale(0) } }
  /* "Spring-loaded" animation */
  path, rect { animation: .1ms hide both paused }
  /* "Hide triggers" above "Show triggers" hide themselves and path */
  rect[fill="red"]:active,
  rect[fill="red"]:active ~ path[stroke="red"] ,
  rect[fill="green"]:active,
  rect[fill="green"]:active ~ path[stroke="green"] {
   animation-play-state: running
  }
  /* "Show trigger" underneath following "Hide trigger" show it and path */
  rect[stroke="red"]:active + rect,
  rect[stroke="red"]:active ~ path[stroke="red"] ,
  rect[stroke="green"]:active + rect,
  rect[stroke="green"]:active ~ path[stroke="green"] {
   animation: none
  }
 </style>
 <!-- Show/Hide "Triggers" -->
 <rect stroke="red" width="30" height="20" x="63" y="3" />
 <rect fill="red" width="30" height="20" x="63" y="3" />
 <rect stroke="green" width="30" height="20" x="63" y="27" />
 <rect fill="green" width="30" height="20" x="63" y="27" />
 <!-- "Graph" -->
 <path stroke="red" d="M0 0 l 20 10 40 40" />
 <path stroke="green" d="M0 50 l 20 -30 40 -20" />
</svg>'>

As indicated in the prologue, this approach is not keyboard-accessible. It is possible to add tabindex="0", or fuse it with <a href="#">nchors, but sadly, keyboard interaction (Enter/Spacebar) does not trigger the :active state the way pointer does. (I guess it should not be this way, but the reality is like it is.)

Variation with constant CSS complexity

And as Kaiido suggested, altering of the source structure by intermixing controls among the target "graph" elements unlocks a major simplification: effectively making the CSS static, i.e., not needing alterations when introducing new interactive content.

Resulting sample with few more shenanigans making the SVG code itself as terse as possible, and without need for repeating anything in the CSS, this time as a regular SO HTML snippet:

@keyframes hide {
 to { transform: scale(0) }
}
path,
rect {
 animation: .1ms hide both paused;
 stroke: currentcolor;
}
rect + rect {
 fill: currentcolor;
}
rect + rect:active,
rect + rect:active + path {
 animation-play-state: running;
}
rect:active + rect,
rect:active + rect + path {
 animation: none;
}
svg {
 fill: none;
 pointer-events: all;
}
rect {
 width: 30px;
 --stroke: 2px;
 stroke-width: var(--stroke);
 height: calc((100% / var(--count)) - 2 * (var(--stroke)));
 x: 63px;
 y: calc(100% / var(--count) * var(--index) + var(--stroke));
}
/*
 Just emulating "sibling count" and "sibling index" for [1..4]
 Cannot wait to actually have it in the CSS
*/
g { --count: 1; --index: 0; }
g:nth-child(2) { --index: 1; }
g:nth-child(3) { --index: 2; }
g:nth-child(4) { --index: 3; }
g:first-child:nth-last-child(2){ &, & ~ g { --count: 2; } }
g:first-child:nth-last-child(3){ &, & ~ g { --count: 3; } }
g:first-child:nth-last-child(4){ &, & ~ g { --count: 4; } }
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 50" height="150">
<g>
 <g color="green"><rect /><rect />
  <path d="M0 50 l 20 -30 40 -20" />
 </g>
 <g color="gold"><rect /><rect />
  <path d="M0 25 l 20 10 40 -10" />
 </g>
 <g color="red"><rect /><rect />
  <path d="M0 0 l 20 10 40 40" />
 </g>
</g>
<!--
 Style moved outside, for nicer SO snippet.
-->
</svg>

(There is just unrelated "sibling count" emulation for making even the placement of "buttons" automatic, that uses nesting for brevity. Besides that the SVG demands on the CSS capabilities remains pretty basic.)

Declarative on/off states

For implementing declarative initial on-off state, we can use some data-attribute as the directive (data-initial="off" here) and flip the direction and button effects. For brevity, custom properties with fallback defaults come in handy:

@keyframes hide {
 to { transform: scale(0); }
}
rect + rect,
rect + rect + path {
 animation: .1ms hide both paused var(--dir, normal);
}
rect:active + rect,
rect:active + rect + path {
 animation-play-state: running;
 animation-name: var(--bottom-act, none);
}
rect + rect:active,
rect + rect:active + path {
 animation-play-state: running;
 animation-name: var(--top-act, hide);
}
[data-initial="off"] {
 --dir: reverse;
 --top-act: none;
 --bottom-act: hide;
}

path, 
rect {
 stroke: currentcolor;
}
rect + rect {
 fill: currentcolor;
}
svg {
 fill: none;
 pointer-events: all;
}
rect {
 width: 30px;
 --stroke: 2px;
 stroke-width: var(--stroke);
 height: calc((100% / var(--count)) - 2 * (var(--stroke)));
 x: 63px;
 y: calc(100% / var(--count) * var(--index) + var(--stroke));
}
/*
 Just emulating "sibling count" and "sibling index" for [1..4]
 Cannot wait to actually have it in the CSS
*/
g { --count: 1; --index: 0; }
g:nth-child(2) { --index: 1; }
g:nth-child(3) { --index: 2; }
g:nth-child(4) { --index: 3; }
g:first-child:nth-last-child(2){ &, & ~ g { --count: 2; } }
g:first-child:nth-last-child(3){ &, & ~ g { --count: 3; } }
g:first-child:nth-last-child(4){ &, & ~ g { --count: 4; } }
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 50" height="150">
<g>
 <g color="green">
  <rect /><rect />
  <path d="M0 50 l 20 -30 40 -20" />
 </g>
 <g color="gold">
  <rect /><rect />
  <path d="M0 25 l 20 10 40 -10" />
 </g>
 <g color="red" data-initial="off">
  <rect /><rect />
  <path d="M0 0 l 20 10 40 40" />
 </g>
</g>
<!--
 Style moved outside, for nicer SO snippet.
-->
</svg>


Notes, Q/A:

Sign up to request clarification or add additional context in comments.

4 Comments

great again ! I'm thinking that your solution is also exponential but you demonstrate plainfully that it is linear. I have also calculated the size of my SVG with 11 simulates checkbox using kaiido and it more than 5MB. When I have some times, I will try both solutions. Thanks for your help.
Awesome. By forcing a DOM structure where the <path> is along the 2 rects you can even avoid repeating the CSS rules: jsfiddle.net/f18ozv20. It can even be further golfed with SVG2 ability to set all presentational attr by CSS: jsfiddle.net/f18ozv20/1 or for older browsers by using <use> to avoid repeating a few attributes: jsfiddle.net/f18ozv20/2
@Kaiido brilliant! Yes, moving the "togglers" in the code structure directly in front of their targets allows simplifying the CSS to basically single construct without any repetition. What a ride, starting at O(2^n) of :target approach, through O(n) of separated :active controls sharing identifiers, to this parametrisable what is O(1)!! (I hope I got the notation right, not formally educated in this field.) BTW, I've realised there are both last approaches present in this older doodle: codepen.io/myf/pen/KKXOyBe?editors=0100
@Kaiido btw, I've compiled a small demo for keyboard vs mouse :active-ation and added it as a remark under most-relevant csswg issue I could find: github.com/w3c/csswg-drafts/issues/7332 . If I understand (HTML) spec correctly, the space bar on focused focusable item should work for triggering the :active state (like it does in Chrome on <button>s), after all. Maybe some day we'll get browsers to actually follow that and do the "space bar click" thing universally?
4

I'll assume your SVGs are embedded either inline or in a frame of some sort.
If they are embedded in an <img>, then you are out of luck, these are not interactive by design.

So if embedded as interactive, you can use a :target hack along with SVG <a> elements as triggers.

<svg>
  <style>
    :has(#element1:target) rect { fill: green }
    :has(#element2:target) rect { fill: blue }
    rect { fill: red }
  </style>
  <a href="#element1" id="element1"><text y="10" x="10">make it green</text></a>
  <a href="#element2" id="element2"><text y="30" x="10" id="element2">make it blue</text></a>
  <rect x="30" y="50" width="50" height="50"/>
</svg>

Here we make the <a> be their own targets, but if it's more pratical for you, you can obviously place the target's id on another element.

One obvious caveat, if embedded inline, is that this will mess with the embedding page's history.

And in its own <iframe>:

document.querySelector("iframe").src = "data:image/svg+xml,"+ encodeURIComponent(`
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="150">
  <style>
    :has(#element1:target) rect { fill: green }
    :has(#element2:target) rect { fill: blue }
    rect { fill: red }
  </style>
  <a href="#element1" id="element1"><text y="10" x="10">make it green</text></a>
  <a href="#element2" id="element2"><text y="30" x="10" id="element2">make it blue</text></a>
  <rect x="30" y="50" width="50" height="50"/>
</svg>
`);
<iframe></iframe>

Though beware regarding <iframe> embedding, you might also face some troubles with CSP, origins, or which protocol is used to embed the file, for instance as srcdoc, this would fail, or here in stacksnippet's null-origined frames, a same-origin blob: URL would also fail (but the same would work in a non-null origin context).

Now, I must admit that to make your whole thing with the full 11 checkboxes will be very tedious. Indeed, the :target trick can only represent a single state. So you need to handle all the possibilities yourself. I invite you to write a program that will do that for you, but to get you started, here is a simple version with only 2 checkboxes toggling to elements. I tried to demonstrate various targeting techniques in it so that you can get creative in your own implementation:

<svg>
  <defs>
    <style>
      svg { --checked-1: none; --checked-2: none; }
      svg:has([id$=t00]:target) { --checked-1: none; --checked-2: none; }
      svg:has([id$=t10]:target) { --checked-1: green; --checked-2: none }
      svg:has([id$=t11]:target) { --checked-1: green; --checked-2: green }
      svg:has([id$=t01]:target) { --checked-1: none; --checked-2: green }
      #rect { fill: red }
      a {
        pointer-events: none;
        use { opacity: 0 }
      }
      svg:not(:has(:target)) [id^=f00] {
        pointer-events: all;
      }
      svg:has([id$=t00]:target) [id^=f00] {
        pointer-events: all;
      }
      svg:has([id$=t10]:target) [id^=f10] {
        pointer-events: all;
      }
      svg:has([id$=t11]:target) [id^=f11] {
        pointer-events: all;
      }
      svg:has([id$=t01]:target) [id^=f01] {
        pointer-events: all;
      }
      /* Logic to target other elements */
      svg:has(:is([id$='1'],[id$='1']):target) {
        #right {
          fill: green
        }
      }
      svg:has(:is([id$='10'],[id$='11']):target) {
        #left {
          fill: green;
        }
      }
    </style>
    <symbol id="checkbox">
      <rect stroke="blue" fill="white" width="20" height="20"/>
      <circle style="fill:var(--checked)" cx="10" cy="10" r="7"/>
    </symbol>
    <symbol id="check-1">
      <use href="#checkbox" style="--checked: var(--checked-1)"/>  
    </symbol>
    <symbol id="check-2">
      <use href="#checkbox" style="--checked: var(--checked-2)" x="30"/>  
    </symbol>
  </defs>

  <use href="#check-1"/>
  <use href="#check-2"/>

  <a href="#f00t10" id="f00t10">
    <use href="#check-1"/>
  </a>
  <a href="#f00t01" id="f00t01">
    <use href="#check-2"/>
  </a>
  <a href="#f01t11" id="f01t11">
    <use href="#check-1"/>
  </a>
  <a href="#f01t00" id="f01t00">
    <use href="#check-2"/>
  </a>
  <a href="#f11t01" id="f11t01">
    <use href="#check-1"/>
  </a>
  <a href="#f11t10" id="f11t10">
    <use href="#check-2"/>
  </a>
  <a href="#f10t00" id="f10t00">
    <use href="#check-1"/>
  </a>
  <a href="#f10t11" id="f10t11">
    <use href="#check-2"/>
  </a>
  <circle id="left" cx="50" cy="80" r="30" fill="red"/>
  <circle id="right" cx="150" cy="80" r="30" fill="red"/>
</svg>

3 Comments

There are eleven distinct toggles in OP's SVG (three in the simplified HTML example). Using :target alone seems to require constructing quite intricate structures for every single combination of toggles (since :target can only be one). Do you think that doing so for such amount (2^11=2048) is manageable?
@myf programmatically, why not? Am I happy it's not my job? Sure.
@myf for instance I doubt Benjamin Aster built his CSS minecraft by typing everything by hand.
1

In my original question, I search to replace <input type="checkbox"> by pure SVG code without using Javascript.

2 solutions have been posted and I appreciate greatly this work.

I post below SVG solution directly in SVG file with 3 checkboxes, 2 checked and 1 unchecked !

I tried a solution using style="display:none" but this doesn't work on Firefox. I tried then using visibility attribute instead of display at that do the job !

#curve-2 { visibility: hidden }

.border
{
border-radius: 2px;
stroke: black;
fill: white;
}
        
@keyframes hide { to { visibility: hidden } }
@keyframes show { to { visibility: visible } }

#check-1 { animation: .1ms hide both paused }
#curve-1 { animation: .1ms hide both paused }
#check-2 { animation: .1ms hide both paused }
#curve-2 { animation: .1ms show both paused }
#check-3 { animation: .1ms hide both paused }
#curve-3 { animation: .1ms hide both paused }

#check-1:active,
:has(#check-1:active) #curve-1
{
animation-play-state: running
}
:has(#border-1:active) #check-1,
:has(#border-1:active) #curve-1
{
animation: none
}

#check-2:active,
:has(#check-2:active) #curve-2
{
animation-play-state: running
}
:has(#border-2:active) #check-2,
:has(#border-2:active) #curve-2
{
animation: none
}

#check-3:active,
:has(#check-3:active) #curve-3
{
animation-play-state: running
}
:has(#border-3:active) #check-3,
:has(#border-3:active) #curve-3
{
animation: none
}
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg id="chi-square_pdf"
  version="1.1"
  baseProfile="full"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  xmlns:ev="http://www.w3.org/2001/xml-events"
  width="720"
  height="600"
  pointer-events="visible"
  >

  <g id="main">
<g id="legends">
  <g transform="translate(10,20)"> 
    <g id="chkbox-1" class="checked">
      <rect id="border-1" stroke="black" fill="white" class="border" width="20" height="20" x="6" y="-12" rx="2" ry="2"/> 
      <g id="check-1">
        <rect id="rect-1" class="check" fill="white" width="16" height="16" x="8" y="-10"/>
        <path id="path-1" class="check" stroke="black" fill="none" stroke-width="2px" d="M 10,-2 l 4,4 l 8,-8"/>
      </g>
    </g>
    <text x="30" y="2" fill="black"> 
      <tspan>circle-1</tspan>
    </text>
  </g>
  <g transform="translate(10,50)"> 
    <g id="chkbox-2" class="unchecked">
      <g id="border-2">
        <rect class="border-check" stroke="black" fill="white" width="20" height="20" x="6" y="-12" rx="2" ry="2"/> 
        <path stroke="black" fill="white" stroke-width="2px" d="M 10,-2 l 4,4 l 8,-8"/>
      </g>
      <rect id="check-2" class="check" fill="white" width="16" height="16" x="8" y="-10"/>
    </g>
    <text x="30" y="2" fill="black"> 
      <tspan>rectangle-2</tspan>
    </text>
  </g>
  <g transform="translate(10,80)"> 
    <g id="chkbox-3" class="checked">
      <rect id="border-3" class="border" stroke="black" fill="white"width="20" height="20" x="6" y="-12" rx="2" ry="2"/> 
      <g id="check-3">
        <rect id="rect-3" class="check" fill="white" width="16" height="16" x="8" y="-10"/>
        <path stroke="black" fill="none" stroke-width="2px" d="M 10,-2 l 4,4 l 8,-8"/>
      </g>
    </g>
    <text x="30" y="2" fill="black"> 
      <tspan>triangle-3</tspan>
    </text>
  </g>
</g>
   
<g id="curves" transform="translate(0, 100)">
  <g id="curve-1" class="curve">
    <circle stroke="blue" stroke-width="2px" fill="none" cx="100" cy="80" r="80"/>
  </g> 
  <g id="curve-2" class="curve">
    <rect stroke="red" fill="none" stroke-width="2px" width="200" height="200" x="40" y="10"/> 
  </g>
  <g id="curve-3" class="curve">
    <path stroke="green" fill="none" stroke-width="2px" d="M100,40 l 100,100, l100,-100 z"/> 
  </g>
</g>
  </g>
</svg>

For information, Wikipedia has accepted SVG file with checkboxes implemented using animations as proposed by @myf.

Graph of chi2 curves

This file can be tested on Wikipedia on https://upload.wikimedia.org/wikipedia/commons/0/0b/Chi-square_pdf.Checkboxes.svg

This solution works on Opera, Microsoft Edge, Chrome and Firefox browsers and on Android phone.

6 Comments

I forgot to mention somewhere around here why I chose transform over display for toggling: Firefox still cannot animate display. bugzilla.mozilla.org/show_bug.cgi?id=1834877
As for "style from inline attribute beats my stylesheet": yes, rules in the inline attribute are always considered "last, so winning in given (author) origin". We can use !important in the stylesheet to "teleport" given rule up to "!important author origin" that win over attribute, unless the attribbute is also there in the !important realm. Rule of thumb, better to avoid this, when uncertain. (Also added some take on the "initial off" toggle into my answer.)
@myf - thanks for information about Firefox. I have replace 'display' by 'visibility' and that doesn't work ! I see that you posted a new solution with 3 checkboxes with one that is unchecked. I didn't dare ask the question, that is why I have posted my proper answer. Il will now investigate your solution. Can you improve your solution in putting all checkboxes in a '<g>' tag and all curves and shapes in another '<g>' tag to see if your solution works in this situation where :has must be used ? Thanks
@myf: I have also used 'display:block!important' in '@keyframes' to force display of hidden curve but this seems not working. My problem with 'visibility' is certainly due to bad use of 'pointer-events: all' in my solution.
@myf: i use now "visibility" instead of "display" or "scale"
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.