4 Ways to Add Click Events with Only CSS Pseudo-Selectors
Note: I'll be using the word target when referring to the element we want to manipulate and trigger as the element we are using to manipulate target.
:checked
Use checkboxes or radios and :checked to determine or cause a target's state and/or to take action.
Trigger
<label>
<input type="checkbox">
 <!--or--> 
<input type="radio">
Conditions
- Requires that the target must be:
- A sibling that follows the trigger or...
- ...a descendant of the trigger.
 
Note
- Hide the actual <checkbox>withdisplay:none
- Ensure that the <checkbox>has anidand that the<label>has aforattribute with a value matching theidof the<checkbox>
- This is dependant upon the target being a sibling that follows the trigger or the target as a descendant. Therefore be aware that you'll most likely use  these selector combinators: ~,+,>.
HTML
<label for='chx'>CHX</label>
<input id='chx' type="checkbox">
<div>TARGET</div>
CSS
#chx:checked + div {...
:target
Use an <a>nchor and apply the :target pseudo-selector on the target element.
Trigger
<a href=""></a>
Conditions
- Assign an idto the target.
- Assign that same idto the<a>hrefattribute preceding with a hash#
HTML
<a href='#target'>A</a>
<div id='target'>TARGET</div>
CSS
#target:target {...
:focus
The trigger element must be either an <input> type or have the attribute tabindex in order to use :focus.
Trigger
<div tabindex='0'>ANY INPUT OR USE TABINDEX</div>
Conditions
- Target must a sibling that is located after the trigger or *target must be a descendant of the trigger.
- State or effect will persist until user clicks elsewhere thereafter a blurorunfocusevent will occur.
HTML
<nav tabindex='0'>
  <a href='#/'>TARGET</a>
  <a href='#/'>TARGET</a>
  <a href='#/'>TARGET</a>
</nav>
CSS
 nav:focus ~ a {...
`:active`
- 
This is a hack that cleverly exploits the transition-delay property in order to actually have a persistent state achieved with no script.
Trigger
<a href='#/'>A</a>
Conditions
- Target must a sibling that is located after the trigger or *target must be a descendant of the trigger.
- There must be a transitionassigned to the target twice.
- The first one to represent the persistent state.
- The second one to represent the normal state.
 
HTML
<a href="#/">A</a>
<div class='target'>TARGET</div>
CSS
.target {
    opacity: 1;
    transition: all 0s 9999999s;
 }
 a:active ~ .target {
     opacity: 0;
     transition: all 0s;
 }
Wacked looking, right? Under normal circumstances, if your trigger had the :active pseudo-selector, we are able to manipulate the target upon keydown. That means our active state is actually active as long as you keep your finger on the button...that's crappy and useless, I mean what are you expected to do to make .active to be useful? Maybe a paperweight and some rubber bands to keep a steady and persistent pressure on the button?
We will leave .active the way it is: lame and useless. Instead:
- Make a ruleset for target under normal circumstances. In the example above it's opacity:1.
- Next we add a transition:...ok then...allwhich works, next is0s...ok so this transition isn't going to be seen it's duration is 0 seconds, and finally...9999999s...116 days delay?
We'll come back to that, we will continue onto the next rulesets...
- These rulesets declare what happens to target under the influence of trigger:active. As you can see that it just does what it normally does, which is onkeydown target will become invisible in 0 seconds. Now once the user keys up, target is visible again...no *target's * new state of opacity:0is persistent! No paperweight, technology has come a long way.
- The target is still actually going to revert back to it's normal state, because :activeis too lazy and feeble to work without rubber bands and paperweights. The persistent state is perceived and not real because target is still leaving the state brought on by:activewhich will be about 116 days before that will happen. ;)
This Snippet features the 4 ways previously mentioned. I'm aware that the OP requested zoom (which is featured therein), but thought it would be to repetitive and boring, so I added different effects as well as zooming.
###SNIPPET
a {
  text-decoration: none;
  padding: 5px 10px;
  border:1px solid red;
  margin: 10px 0;
  display: inline-block;
}
label {
  cursor: pointer;
  padding: 5px 10px;
  border: 1px solid blue;
  margin: 10px 0;
  display:inline-block;
}
button {
  cursor:pointer;
  padding: 5px 10px;
  border: grey;
  font:inherit;
  display:inline-block;
}
img#img {
  width: 384px;
  height: 384px;
  display: block;
  object-fit: contain;
  margin: 10px auto;
  transition: width 3s height 3s ease-in;
  opacity: 1;
  transition: opacity 1s 99999999s;
}
#zoomIn,
#zoomOut,
#spin {
  display: none;
  padding: 0 5px;
}
#zoomOut:checked + img#img {
  width: 128px;
  height: 128px;
  transition: all 3s ease-out;
}
#zoomIn:checked + img#img {
  width: 512px;
  height: 512px;
  transition: all 3s ease-in-out;
}
  
#spin:checked ~ img#img {
  transform: rotate(1440deg);
}
img#img:target {
  box-shadow: 0px 8px 6px 3px rgba(50, 50, 50, 0.75);
}
a.out:focus ~ img#img {
  opacity: 0;
  transition: opacity 1s;
}
a.in:active ~ img#img {
  opacity: 1;
  transition: opacity 1s;
}
.grey:focus ~ img#img {
  filter: grayscale(100%);
}
<a href='#/' class='out'>FadeouT</a><a href='#/' class='in'>FadeiN</a>
<a href='#img'>ShadoW</a>
<br/><button class='grey' tabindex='0'>GreyscalE</button><br/>
<label for='spin'>SpiN</label>
<input type='checkbox' id='spin'>
<label for='zoomIn'>ZoomiN</label>
<input type='radio' id='zoomIn' name='zoom'>
<label for='zoomOut'>ZoomouT</label>
<input type='radio' id='zoomOut' name='zoom'>
<img id='img' src='https://i.ibb.co/5LPXSfn/Lenna-test-image.png'>