<!-- demo shown on https://dev.to/fcalderan/a-css-carousel-with-snapping-points-and-a-scroll-linked-navigation-5h6j -->

<section class="slider">
   <div>
      <ul id="s">
         <li id="s1">
            <img src="//fabrizio.dev/demo/tesseract1.jpg" 
                 alt="Daniel Tompkins (Portals 2020)" />
         </li>
         <li id="s2">
            <img src="//fabrizio.dev/demo/tesseract2.jpg" 
                 alt="TesseracT (Portals 2020)" />
         </li>
         <li id="s3">
            <img src="//fabrizio.dev/demo/tesseract3.jpg" 
                 alt="Amos Prem Williams (Portals 2020)"/>
         </li>
         <li id="s4">
            <img src="//fabrizio.dev/demo/tesseract4.jpg" 
                 alt="TesseracT (Portals 2020)"/>
         </li>
         <li id="s5">
            <img src="//fabrizio.dev/demo/tesseract5.jpg" 
                 alt="TesseracT (Portals 2020)"/>
         </li>
      </ul>
   </div>
   
   
   <nav>
      <a href="#s1" title="title slide 1">
         Go to Slide 1</a>
      <a href="#s2" title="title slide 2">
         Go to Slide 2</a>
      <a href="#s3" title="title slide 3">
         Go to Slide 3</a>
      <a href="#s4" title="title slide 4">
         Go to Slide 4</a>
      <a href="#s5" title="title slide 5">
         Go to Slide 5</a>
   </nav>   
</section>
.slider div {
   /* need to hide the horizontal scrollbar of 
    * the inner list
    */
   overflow: hidden;   
   
   /* using logical properties instead of
    * width and/or height
    */
   inline-size: 80%;
   min-inline-size: 300px;
   max-inline-size: 680px;   
   margin: 4rem auto;
   aspect-ratio: 2 / 1;
   border: 1px #e2e2e2 solid;
   box-shadow: 0 0 60px rgba(0, 0, 20, .6);
   border-radius: 1rem;
}


.slider ul {
   display: flex;
   flex-flow: row nowrap;
   overflow-x: scroll;
   scroll-snap-type: x mandatory;
   scroll-behavior: smooth;
   cursor: ew-resize;   
   
   /* clip away the scrollbar */
   block-size: calc(100% + 25px);
   
}

.slider li {
   flex: 0 0 100%;
   scroll-snap-align: start;
   scroll-snap-stop: always;
   /* restore the actual height of the slider */
   block-size: calc(100% - 25px);
}

.slider img {
   object-fit: cover;
   max-inline-size: 100%;
   block-size: 100%; 
}


.slider nav {
   position: relative;
   display: flex;
   margin: 0 auto;
   inline-size: max-content;
   
   /* DRY: set here the gap between the dots,
    * we will need it later to set the position
    * of the nav::before pseudoelement
    */
   --gap: .8rem;
   gap: var(--gap, .5rem);
}




.slider nav::before, 
.slider nav a {
   inline-size: 1rem;
   aspect-ratio: 1;
   border-radius: 50%;
   background: #9bc;
}

.slider nav a {
   text-indent: 100%;
   overflow: hidden;
   white-space: nowrap;
   opacity: .33;
}


.slider nav::before {
   content: "";
   position: absolute;
   z-index: 1;
   display: block;
   cursor: not-allowed;


   /* here we use the above --gap variable.
    * the --slide variable is set in the
    * keyframes below.
    */
   transform: translateX(
      calc((100% + var(--gap, .5rem)) * 
      calc(var(--slide, 1) - 1))
   );
   animation: dot 1s steps(1, end) forwards;
   /* shhh... here is the magic */
   animation-timeline: slide;
}


/* timeline animation */
@scroll-timeline slide {
   source: selector(#s);
   orientation: inline; 
   time-range: 1s;
}

@keyframes dot {
   0%    { --slide: 1; }
   12.5% { --slide: 2; }
   37.5% { --slide: 3; } 
   62.5% { --slide: 4; }
   87.5%, 100% { --slide: 5; }
}
/* no JS */

External CSS

  1. https://fonts.googleapis.com/css2?family=Hachi+Maru+Pop&amp;display=swap

External JavaScript

This Pen doesn't use any external JavaScript resources.