CARVIEW |
Select Language
HTTP/2 200
accept-ranges: bytes
age: 2
cache-control: public,max-age=0,must-revalidate
cache-status: "Netlify Edge"; fwd=miss
content-encoding: gzip
content-type: text/html; charset=UTF-8
date: Sat, 11 Oct 2025 08:01:55 GMT
etag: "c4b9480c1da7285a6b3ae0cec1a1cdee-ssl-df"
server: Netlify
strict-transport-security: max-age=31536000; includeSubDomains; preload
vary: Accept-Encoding
x-nf-request-id: 01K794MRJAAPNM0WRCS4JGNZHJ
Eleventy WebC Progressive Enhancement Recipes
Progressive Enhancement Recipes using Eleventy WebC Image Comparison Components
Return to the GitHub repository.
The examples below are in order of implementation complexity from least to most. All are using is-land
, specifically <is-land on:visible>
. The pre-JS experience is emulated using <is-land on:media="(max-width: 0px)">
.
Summary
- If you want to show both images pre-JS, this can be solved with an entirely clientside Image Comparison component but it will negatively impact your Content Layout Shift score. This use case is not shown below.
- Do you want to show either the left or right image pre-JS? Use
@value="0"
or@value="100"
respectively. - Do you want the form control to be interactive pre-JS? Go to the Always Enabled approach.
- If you want an interactive form control pre-JS and have it be functional too? See the
:has()
and Radios approach. - Does this form input need to be a successful form control and submit to the server? The ShadowDOM example is the only one that doesn’t work with forms. This *could* be fixed with JS but is it worth it?
Table of Contents
- Always Enabled
- Disabled until JavaScript
- Hidden via
:not(:defined)
CSS - Hidden via JS
<template>
- Declarative Shadow DOM
:has()
and Radios- Bonus:
opacity
Slider
Always Enabled
- Complexity Level: Low
- The range control is always enabled and synchronized to the display when JS initializes.
- This has the potential for an uncanny valley in which the control is movable but the view is not updated!
- WebC source code
<!-- WebC Component HTML -->
<image-compare-enabled @name="range-enabled" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-enabled>
Disabled until JavaScript
- Complexity Level: Low
- This version uses JS to toggle the range input from disabled to enabled.
- Disabling the control avoids the uncanny valley (in comparison to keeping it enabled) by communicating to the user that the control is not yet initialized.
- WebC source code
<!-- WebC Component HTML -->
<image-compare-disabled @name="range-disabled" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-disabled>
Hidden via CSS
- Complexity Level: Low
- This version uses CSS
:not(:defined)
to hide the range input before JS. - WebC source code
<!-- WebC Component HTML -->
<image-compare-defined @name="range-hidden" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-defined>
Hidden via JS Template
- Complexity Level: Low
- This version uses JS to swap in
<template>
content holding the range input. - WebC source code
<!-- WebC Component HTML -->
<image-compare-hidden @name="range-hiddentmpl" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-hidden>
Declarative Shadow DOM
- Complexity Level: Moderate
- This version uses Declarative Shadow DOM for the range input. Before JS, the range input is always enabled and synchronized to the display when JS runs.
- When Declarative Shadow DOM is not supported, the polyfill is applied inline (it’s a few lines of code). Before JS, the range input is hidden.
- Benefit: we can move some of the styles into Shadow DOM to scope those styles for free!
- Drawback: This version does not have a successful form control because it uses Shadow DOM. This can be fixed with JS (but requires JS).
- Drawback: Is there a way to de-duplicate the declarative shadow dom template without JavaScript? Does every instance of the component need the same nested markup?
- WebC source code
<!-- WebC Component HTML -->
<image-compare-shadowdom @name="range-shadowdom" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-shadowdom>
:has()
and Radios
- Complexity Level: Moderate
- This version uses
:has
and radios for a functional No-JS experience (and synchronizes the value pre-JS and post-JS). Of course, the number of radios shown can be customized. - If
:has()
is not supported (at time of writing, Firefox), the inputs are hidden (though you could modify this behavior to be Always Enabled and synchronized). - WebC source code
<!-- WebC Component HTML -->
<image-compare-nojs @name="range-radios" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-nojs>
Bonus: Opacity Slider
For fun you can change clip-path: inset(0 0 0 var(--position));
to opacity: var(--position);
on any of these components to have the slider vary opacity instead of clip amount.
<!-- WebC Component HTML -->
<image-compare-defined @name="range-opacity" @value="50">
<img src="/javaskipped-a.png" alt="JavaSkipped logo left" width="800" height="800" loading="lazy" style="clip-path: none; opacity: var(--position);">
<img src="/javaskipped-b.png" alt="JavaSkipped logo right" width="800" height="800" loading="lazy">
</image-compare-defined>