Sliders, also known as carousels, are a popular tool for modern websites. They enable efficient transmission of information within a limited space.
Developers often rely on frameworks to build sliders. Although productive, this method does not contribute much to your growth as a developer.
Therefore, in this tutorial, we’ll adopt a more traditional approach to this project by using plain HTML, CSS, and JavaScript.
Codepen here:
Warming up
Before proceeding, I want to introduce two fundamental concepts that this project relies on.
Firstly, when you link a label
element to an input
element, like this:
<input type="radio" id="radio1"/>
<label for="radio1">label for radio1</label>
Clicking that label on the webpage passes the focus to the linked input element, as shown below:
This behavior is consistent even when you reposition either of the elements using CSS:
label{
position: fixed;
top: 10rem;
right: 5rem;
}
The output:
Furthermore, you can apply this concept to other input types, such as checkboxes, numbers, and text fields. This linking expands the clickable area of the input element, and, as you will discover later, it allows for some neat tricks.
Secondly, using CSS pseudo-classes and combinators, you can dynamically change the styling of an element based on the current state of another element.
Pseudo-classes allow you to style specific states of elements. For example, :hover
targets an element’s state when you hover over it with your mouse.
p{
color: black;
}
p:hover{
color: red;
}
Note that all pseudo-classes begin with a single colon (:).
Several pseudo-classes in CSS allow you to target various states of elements, but I’ll draw your attention to the :checked
pseudo-class in this tutorial.
:checked
is used to target the state when you toggle on a radio input or checkbox. For example, let’s consider our previous input element:
input:checked{
outline: 2px solid red;
outline-offset: 10px;
}
This output:
You can see that the browser applied the styles only after you toggled the radio on.
Alternatively, combinators establish the connection between selectors. There are four types of combinators in CSS:
- General sibling combinator (~)
- Adjacent sibling combinator (+)
- Child combinator (>)
- Descendant combinator (space)
Here, we’ll focus on the general sibling combinator. The general sibling combinator allows you to style sibling elements in your HTML.
For example, consider the following CSS rule:
.child1 ~ .child2{
/* some declarations */
}
This selector initially identifies any element in our HTML with a class of .child1
. It then applies the specified styles to any of its younger siblings with a class of .child2
, as demonstrated below:
You should note the following about any two elements connected in this way:
- They need to be sibling elements, which means they must share a common parent element, as shown above.
- They do not need to be immediate siblings. This combinator disregards any other siblings placed between them.
- Finally, you cannot apply combinator styles upwards in the HTML tree, only downwards. Thus,
.child2
must come after.child1
, making.child2
a younger sibling.
Putting it all together, and still on our input and label elements from before, we have:
input:checked ~ label {
outline: 2px solid red;
outline-offset: 10px;
}
The output:
As you can see, we dynamically changed the styling of a sibling element(label
) based on the :checked
state of the input
element.
With all that out of the way, we can continue with our program.
The HTML
Our first task is to create a section
of class slider
within the body
element.
<body>
<section class="slider"></section>
</body>
Within this section
, we’ll add:
- Four radio inputs, one for each slide. Each radio input should have a unique
id
. - An unordered list (
ul
) with classslides-flex
. It should contain four list elements (li
), each assigned a classslide
. Theseli
will serve as our individual slides, containing whatever we wish to to display. - A
div
with classnavigation
. This should contain four label elements, each linked to one of the four radio inputs previously mentioned. Each label element should also have a uniqueclass
and anid
between 1 and 4, corresponding to its position in the parent element.
<body>
<section class="slider">
<!-- radio buttons start -->
<input type="radio" name="radio-btn" id="radio1" />
<input type="radio" name="radio-btn" id="radio2" />
<input type="radio" name="radio-btn" id="radio3" />
<input type="radio" name="radio-btn" id="radio4" />
<!-- radio buttons end -->
<!-- slides start -->
<ul class="slides-flex">
<li class="slide">
<h2>1</h2>
</li>
<li class="slide">
<h2>2</h2>
</li>
<li class="slide">
<h2>3</h2>
</li>
<li class="slide">
<h2>4</h2>
</li>
</ul>
<!-- slides end -->
<!-- navigation start -->
<div class="navigation">
<label for="radio1" class="btn1" id="1"></label>
<label for="radio2" class="btn2" id="2"></label>
<label for="radio3" class="btn3" id="3"></label>
<label for="radio4" class="btn4" id="4"></label>
</div>
<!-- navigation end -->
</section>
</body>
Our HTML section is now complete. You can copy this into your editor.
As you can see, our webpage is starting to take shape:
The CSS
You can adapt most of the styling of this project to your specific use case. However, in this section, I’ll explain the fundamental style decisions that make this project work.
Global Styling
Firstly, let’s remove the default margins, paddings, and box-sizing present on each element using the global selector (*
). This improves control over element styling and reduces variation across browsers.
*{
margin: 0;
padding: 0;
box-sizing: border-box;
}
Next, the :root
selector allows you to create custom variables that can reused throughout your style sheet. We’ll use color: #333;
numerous times in our style sheet, and creating a custom variable is essential to avoid repetition.
:root{
--primaryColor: #333;
}
Note that we can later access this custom variable using
color: var(--primaryColor);
.
Now, for our body
element:
body {
display: grid;
color: var(--primaryColor);
height: 100vh;
place-items: center;
background-color: rgb(208, 230, 249);
font-family: sans-serif;
}
ul {
list-style: none;
}
Here, we set the following properties:
-
background-color
: To provide a consistent visual backdrop for our webpage. -
color
andfont-family
: We apply these styles globally to all child elements. -
display: grid;
andplace-items: center;
: When used together, they center all child elements. It’s important to note that the parent element requires a specifiedheight
property for this method to work. Our height in this case, spans the entire viewport (100vh). -
list-style: none;
: To remove the bullets from all list elements on our webpage.
Our webpage should look like this:
Class .slider
This will serve as a container to showcase our slides.
.slider {
position: relative;
width: 80%;
aspect-ratio: 1;
max-width: 50rem;
max-height: 31.25rem;
border-radius: 0.7rem;
/*overflow: hidden;*/
cursor: grab;
border: 2px solid green;
}
.slider input {
display: none;
}
-
position: relative;
: This accommodates the later placement of absolutely positioned elements. -
aspect-ratio: 1;
: Ensures the element remains square with equal sides, eliminating the need to set the height property for improved responsiveness. -
max-height
andmax-width
: Set limits on how large the element can become on larger screen sizes. -
overflow: hidden;
: We will discuss more on this later. -
cursor: grab;
: Indicates that the element is draggable. -
display: none;
: Hides the element from the viewport. Note that we can still access the radio inputs programmatically even though they are invisible.
The output:
Note that the borders are only present for visualization purposes, and we’ll remove them later.
Class .slides-flex
In this section, we’ll arrange the slides horizontally using Flexbox.
Moreover, this section serves as the dynamic part of our project. To achieve this, it needs to have absolute positioning so we can adjust it relative to its relatively positioned parent(.slider
).
.slides-flex {
display: flex;
width: 400%;
height: 100%;
position: absolute;
left: 0;
transition: 0.8s;
}
-
width: 400%;
: This sets the width to four times that of its parent. -
height: 100%;
: Stretches the element to fill the entire height of its parent. -
position: absolute;
: Enables us to use theleft: 0;
declaration relative to its parent, placing it at the left edge of .slider. -
transition: 0.8s;
: Used to create a smooth motion and introduce a sliding effect.
Class .slide
In this section, we’ll style each of our slides. We’ll start with the general layout by styling all elements with a class of .slide
. We’ll then target each slide using the :nth-child
pseudo-class.
.slide {
width: 25%;
display: grid;
place-items: center;
}
.slide:nth-child(1) {
background-color: #fed050;
border-radius: 0.7rem 0 0 0.7rem;
}
.slide:nth-child(2) {
background-color: #7fdbee;
}
.slide:nth-child(3) {
background-color: #4cf89c;
}
.slide:nth-child(4) {
background-color: #f79191;
border-radius: 0 0.7rem 0.7rem 0;
}
.slide h2 {
user-select: none;
height: 5rem;
width: 5rem;
font-size: 3rem;
border-radius: 50%;
display: grid;
place-items: center;
text-align: center;
outline: 3px solid;
}
-
border-radius
: This property can accept one to four values. When using four values, they are applied clockwise starting from the top-left corner. In our first slide (0.7rem 0 0 0.7rem
), we aim to round the top-left and bottom-left corners. Conversely, for our fourth slide (0 0.7rem 0.7rem 0
), we intend to round the top-right and bottom-right corners. You’ll see the importance of this in the JavaScript part. -
user-select: none;
: Prevents unnecessary text highlighting while dragging each slide with the mouse.
The output:
Class .navigation
For our navigation, let’s style each label
to look like radio inputs.
navigation {
position: absolute;
display: flex;
width: 100%;
bottom: 2.5rem;
gap: 2rem;
justify-content: center;
}
.navigation label {
border: 2px solid var(--primaryColor);
padding: 0.3rem;
border-radius: 50%;
cursor: pointer;
transition: 1s;
}
-
position: absolute;
: Enables the use ofbottom: 2.5rem;
, positioning it 2.5rem from the bottom of.slider
. -
cursor: pointer;
: Indicates to the user that the element is clickable. -
border-radius: 50%;
: Transforms each label into a circle.
Here’s what the navigation should look like:
On :checked
As mentioned earlier, the :checked
pseudo-class allows access to the checked state of radio inputs. And when used with the general sibling combinator (~), grants access to their younger siblings:
- Class
.slides-flex
, and - Class
.navigation
.
Starting with .slides-flex
:
#radio1:checked ~ .slides-flex {
left: 0;
}
#radio2:checked ~ .slides-flex{
left: -100%;
}
#radio3:checked ~ .slides-flex {
left: -200%;
}
#radio4:checked ~ .slides-flex {
left: -300%;
}
In CSS, when you use a percentage value to position an element, that value is relative to the width of its parent element. So, when you set left: -100%;
, it shifts the element to the left, outside its parent element, at a distance equal to 100% of its parent’s width, as demonstrated below:
As a reminder, we previously defined the width of .slides-flex
to be four times(400%) that of .slider
. Therefore, setting left: -100%;
simply shifts .slides-flex
by one-quarter of its width to the left, bringing a new slide to focus.
Looking at the image below, clicking each button after the first subtracts an additional 100% from the position of.slides-flex
, bringing the slide at that position into focus.
Now for .navigation
, so we can know what slide we’re in:
#radio1:checked ~ .navigation .btn1 {
background-color: var(--primaryColor);
}
#radio2:checked ~ .navigation .btn2 {
background-color: var(--primaryColor);
}
#radio3:checked ~ .navigation .btn3 {
background-color: var(--primaryColor);
}
#radio4:checked ~ .navigation .btn4 {
background-color: var(--primaryColor);
}
Notice how, first, we selected the parent element of the buttons, which is a sibling to the radio inputs. Then, we accessed each button using the descendant combinator (space).
Now, when we click any button, it changes its background color then brings a corresponding slide into focus, as shown below:
Rounding up, to ensure that only the selected slide is visible, let’s uncomment the line overflow: hidden;
in our .slider
styling. This change hides content outside the boundaries of the parent element, which, in this case, includes all other slides.
We can also comment out the border styling while at it:
.slider{
...
overflow: hidden;
...
/* border: 2px solid green; */
}
The output:
Finally, if you look at the image above, you’ll see that upon reload, no label is checked, and users won’t know what slide they are currently on. To avoid this, let’s add the checked
attribute to our #radio1
. This checks the first radio input on every reload, indicating that we are currently on the first slide.
<input type="radio" name="radio-btn" id="radio1" checked />
The output:
That concludes the CSS part. So far, we’ve implemented the sliding animations and manual navigation. Regrettably, for the sake of brevity, we must pause here. I’ll release a sequel to this article covering the JavaScript part, allowing us to achieve the:
- Autoplay, and
- Dragging (mouse) or swiping (touchscreen) options. Stay tuned!
Conclusion
In this tutorial, we’ve learned that not all webpage functionality needs to rely solely on JavaScript. By utilizing CSS pseudo-classes and combinators, we dynamically modified our webpage elements—a task that might otherwise have cost us several lines of JavaScript code.
Finally, if you’ve learned anything new, consider dropping a like and sharing so others can benefit as well. Until next time.
Some Helpful Links
Find out more on:
Top comments (4)
Thanks
You’re welcome✨
This looks neat. Will your next article also cover accessibility?
Thanks a lot!
I understand the place of accessibility in projects like these. However, this tutorial aimed to keep things as stripped-down as possible, allowing for future modification. While adhering to front-end best practices, my primary focus was on clarity.
Nevertheless, I plan to write additional articles addressing accessibility in more detail.