How to Build an Image Slider With HTML and CSS
Build a responsive, keyboard-friendly image slider with semantic HTML and CSS scroll snap, with complete code, accessibility notes, and troubleshooting.

A simple image slider can be built with semantic HTML and CSS—no JavaScript required. Put each image in a list item, make the list a horizontally scrollable container, and use CSS Scroll Snap to align each slide when the reader scrolls. The browser supplies the scrolling; the CSS supplies the layout and snap points. This version works with touch, a trackpad, a mouse, and keyboard scrolling, and it does not move slides automatically.
Save the following as index.html and open it in a browser. Replace the example image URLs and alt text with your own images and descriptions.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CSS image slider</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
padding: 2rem 1rem;
color: #172033;
background: #f5f7fb;
font: 1rem/1.5 system-ui, sans-serif;
}
.gallery {
width: min(100%, 52rem);
margin-inline: auto;
}
.gallery h1 { margin-block: 0 0.5rem; }
.gallery p { margin-block: 0 1rem; }
.slides {
display: grid;
grid-auto-columns: 100%;
grid-auto-flow: column;
gap: 1rem;
overflow-x: auto;
overscroll-behavior-inline: contain;
scroll-snap-type: inline mandatory;
scroll-padding-inline: 0;
padding: 0 0 0.75rem;
margin: 0;
list-style: none;
}
.slide {
min-width: 0;
scroll-snap-align: start;
overflow: hidden;
border-radius: 0.75rem;
background: white;
box-shadow: 0 0.25rem 1rem rgb(20 35 60 / 12%);
}
.slide img {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
.slide figcaption { padding: 0.75rem 1rem; }
.slides:focus-visible {
outline: 3px solid #175cd3;
outline-offset: 4px;
}
@media (prefers-reduced-motion: no-preference) {
.slides { scroll-behavior: smooth; }
}
@media (max-width: 36rem) {
body { padding: 1rem 0.75rem; }
.slides { grid-auto-columns: 92%; }
}
</style>
</head>
<body>
<main class="gallery">
<h1>Coastal walks</h1>
<p>Scroll horizontally to browse the photographs.</p>
<ul class="slides" aria-label="Coastal walk photographs" tabindex="0">
<li class="slide">
<figure>
<img src="https://images.unsplash.com/photo-1500375592092-40eb2168fd21?auto=format&fit=crop&w=1400&q=80" alt="Waves breaking over a rocky shoreline">
<figcaption>Rocky shoreline</figcaption>
</figure>
</li>
<li class="slide">
<figure>
<img src="https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=1400&q=80" alt="Mountain peaks beneath a clear sky" loading="lazy">
<figcaption>Mountain light</figcaption>
</figure>
</li>
<li class="slide">
<figure>
<img src="https://images.unsplash.com/photo-1470770841072-f978cf4d019e?auto=format&fit=crop&w=1400&q=80" alt="A lake surrounded by forested mountains" loading="lazy">
<figcaption>Still water</figcaption>
</figure>
</li>
</ul>
</main>
</body>
</html>
1. How the slider works
The <ul> is both the list of slides and the scroll container. Its grid layout places list items in columns flowing along the inline axis. grid-auto-columns: 100% makes each column as wide as the container, while overflow-x: auto enables horizontal scrolling when the slides do not fit. A scroll container needs a defined size and overflow for scrolling to occur. MDN explains the basic scroll snap requirements.

scroll-snap-type: inline mandatory belongs on the container. It tells the browser to settle scrolling at a snap position along the inline axis. scroll-snap-align: start belongs on each slide and defines its snap position. Together, they create predictable stopping points while leaving movement to native browser scrolling. CSS Scroll Snap defines how panning and scrolling settle at positions in a scroll container; it does not create buttons or announce slide changes by itself. See MDN’s CSS Scroll Snap guide.
2. Build and customize it step by step
Step 1: Use meaningful structure
Give the gallery a heading, then group the slides in a list. A figure and caption are useful when each image needs a visible description. Write alt text that communicates the important content of each image; do not copy a caption into alt text if that would make the same information repetitive for screen-reader users. If an image is purely decorative and the caption already conveys its meaning, an empty alt="" may be appropriate.
Step 2: Make slides responsive
The desktop layout shows one full slide at a time. At a narrow viewport the example uses grid-auto-columns: 92%, leaving a glimpse of the next slide so readers can discover that more content is available. Use 100% if you prefer a single-slide view at every size. The images use width: 100% and aspect-ratio to reserve space before loading; object-fit: cover fills the frame and crops excess image area. Choose contain instead when cropping would hide important details.
Step 3: Choose snap behavior deliberately
mandatory makes the browser settle on a slide after scrolling. It fits this example because each slide is a simple, fixed-height image and caption. It can cause a real usability problem when content inside a slide is taller or wider than the visible area: users may be unable to scroll to the overflow. MDN specifically cautions about this. Use scroll-snap-type: inline proximity if snapping should happen only when the scroll ends near a snap point, or make the slide itself scrollable when its contents need independent scrolling. Test long captions, zoomed text, and small screens.
Step 4: Keep focus and motion understandable
The tabindex="0" makes the scroll container keyboard-focusable, and the visible focus outline shows where focus is. Users can scroll it with available browser and keyboard controls. The CSS does not provide previous/next buttons or a live announcement of the current slide. Add explicit controls if the design requires them; give each control an accessible name, use native buttons, and keep controls large enough to operate. WAI recommends at least 44 × 44 CSS pixels for buttons and links that are not inline in text, and sufficient contrast with their backgrounds. See WAI carousel styling guidance.
This example does not autoplay. If you later add automatic movement, provide a way to pause it. WAI says users must be able to pause carousel movement because it can be too fast or distracting and make text hard to read. A semantic list and CSS alone do not add slide announcements or control behavior; consult the WAI Carousels Tutorial when adding those features.
3. Useful CSS options
| Property or choice | Where it goes | Effect |
|---|---|---|
overflow-x: auto |
Scroll container | Allows horizontal scrolling when content exceeds the available width. |
scroll-snap-type: inline mandatory |
Scroll container | Requires scrolling to settle at a snap position on the inline axis. |
scroll-snap-type: inline proximity |
Scroll container | Snaps when a scroll ends near a snap point; allows freer stopping elsewhere. |
scroll-snap-align: start |
Each slide | Aligns a slide’s start with the container’s snap area. |
scroll-padding-inline |
Scroll container | Adjusts the effective snap area, useful when the container has inset controls or padding. |
scroll-margin-inline |
Each slide | Adjusts an individual slide’s snap area. |
scroll-behavior: smooth |
Scroll container | Makes programmatic or anchor scrolling smooth where supported; it is not a control system. |
object-fit: cover |
Image | Fills the frame and crops to fit; use contain to preserve all image content. |
The logical inline axis follows writing direction, so the snap configuration adapts to the document’s direction. If you use a horizontal-only design intentionally, x is another axis value. Consult MDN’s scroll snap references for the complete behavior and browser-specific details.
4. Optional CSS carousel controls
Newer CSS carousel features can generate scroll buttons and slide markers with pseudo-elements such as ::scroll-button(), ::scroll-marker-group, and ::scroll-marker. MDN documents these patterns in its CSS carousel guide. They may let the browser handle navigation without application JavaScript, but do not assume they work in every browser your audience uses. Check current support for your actual browser targets before depending on them.
Generated controls also need accessible names and verification with the intended assistive technologies. MDN notes that alternative text for generated button content can supply accessible names in some user agents, while warning that this may not be sufficient for WCAG conformance. Treat the generated controls as an enhancement and retain usable native scrolling. If older or unsupported browsers matter, provide a tested fallback, such as explicit HTML buttons and JavaScript, rather than leaving navigation dependent on an unsupported feature.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| All slides appear in a vertical column. | The list is not laid out horizontally, or a competing rule overrides the grid. | Confirm the container has display: grid, grid-auto-flow: column, and an appropriate grid-auto-columns. |
| There is no horizontal scrolling. | The slides fit within the container, or overflow is not enabled. | Check overflow-x: auto on the list and ensure the combined slide widths plus gaps exceed the container width. |
| Scrolling works but does not snap. | The snap rule is missing or applied to the wrong element. | Put scroll-snap-type on the actual scrolling element and scroll-snap-align on each slide. Check whether another stylesheet overrides either rule. |
| A slide stops partly offscreen. | Slide width, gap, padding, or snap alignment does not match the intended layout. | Inspect the computed widths. For a one-slide view, use full-width columns, start alignment, and matching scroll padding. Recheck after adding container padding. |
| Part of a caption or image cannot be reached. | Mandatory snapping is trapping overflowing slide content. | Use proximity snapping, shorten or reflow the content, or let the overflowing region scroll independently. Check at zoom and narrow viewport sizes. |
| The image is stretched or an important subject is cropped. | The frame ratio differs from the source and cover cropping is active. | Adjust aspect-ratio, choose a better source crop, or set object-fit: contain. |
| A slide is blank. | The image URL is invalid, blocked, or still loading. | Open the URL directly, inspect the browser network panel, and use a valid image response. Add a local fallback or error state if images are essential. |
| Keyboard users cannot find the slider. | The container is not focusable or has no visible focus style. | Keep tabindex="0" and a high-contrast :focus-visible outline; test tab order and keyboard scrolling. |
6. Performance, reliability, and cost
A CSS scroll slider has no slider script to download or initialize, and native scrolling benefits from browser input handling. Actual image loading and rendering still determine much of the page’s cost. Serve images near the size they will be displayed, compress them appropriately, reserve layout space with dimensions or aspect-ratio, and lazy-load slides that begin offscreen. Keep the first visible image eager so the initial view is not delayed by lazy loading.
Use stable image hosting and provide useful alt text if an image fails. Lazy loading can save initial network work for long galleries, but it may make an image appear only as the user approaches it; verify that behavior on the devices and network conditions that matter to your audience. The browser owns scrolling and snapping, so there is no timer or slider state to drift out of sync. However, the CSS-only example does not provide autoplay, arrows, slide counters, or announcements. Add those only when the interface needs them and implement their keyboard and assistive technology behavior deliberately.
The implementation itself has no library or service cost. Costs can still come from image hosting, bandwidth, and the time needed to produce and maintain accessible content. If you need to capture a rendered page for review, documentation, or a visual regression artifact, ScreenshotNeo offers a website screenshot API and MCP server. Its API returns PNG, JPEG, WebP, or PDF from a GET request, and its feature set includes full-page capture and custom viewport options. See ScreenshotNeo for the product overview.
7. Or skip the browser setup
To capture a page image with ScreenshotNeo, make one request. See the ScreenshotNeo API documentation for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Replace YOUR_API_KEY and the target URL. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed; responses include page-verdict and billing headers. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.
Sign up for ScreenshotNeo’s free plan and get 1,000 screenshots a month with no card.
8. Frequently asked questions
Can I make an image slider with only HTML and CSS?
Yes. Native horizontal scrolling plus CSS Scroll Snap creates the basic interaction without JavaScript. Add JavaScript only if you need behavior such as custom controls, autoplay, or announcements that the basic pattern does not provide.
Does a CSS-only slider automatically add arrow buttons?
No. The scrollable list can be moved using native scrolling. Newer CSS carousel pseudo-elements can provide generated controls in supporting browsers, but verify support and accessibility for your target audience.
Should every slider autoplay?
No. This example leaves movement under the reader’s control. If you add automatic movement, include a pause mechanism and follow the WAI carousel guidance.
What is the difference between mandatory and proximity?
Mandatory snapping settles at a snap point; proximity snapping only does so when a stopping position is near one. Choose based on whether content can overflow and how much free scrolling the design needs.
Can I use this pattern for product cards instead of images?
Yes. Each list item can contain a card, but test the card’s full content at zoom and on narrow viewports. Avoid mandatory snapping if it makes any content unreachable.


