How to Render a Carousel from a Template
Render reusable carousel slides from data, add accessible controls, and choose the right template, Bootstrap, or CSS approach.

A template carousel takes a collection of records, renders one slide for each record, and adds controls for moving between slides. Start by defining the fields each slide needs, then bind those fields in your template and emit semantic markup. Add real previous and next buttons, keyboard support, and a clear way to announce slide changes. If the content does not need rotation, a static list or grid is often easier to discover.
This guide walks through the data contract, a reusable server-rendered template, a complete accessible JavaScript carousel, and Bootstrap and CSS alternatives. The exact template syntax depends on your platform: Microsoft Power Pages uses FetchXML and Liquid, while other CMSs and component systems have their own query and binding tools.
1. Decide whether a carousel fits
A carousel presents a subset of content at a time. That can save space, but it can also make items harder to discover. Before writing the template, decide whether people benefit from seeing one item at a time. A grid or ordinary list is a good fit when users need to scan, compare, or browse all records.
For a carousel, decide these details up front:
- How many records should it show, and what should happen when there are none?
- Does it show one slide at a time or several cards in a row?
- Will people navigate manually, or is rotation necessary?
- Can every slide link to a detail page, and what content is meaningful without its image?
- What is the intended behavior on narrow screens, with keyboard input, and with reduced motion?
2. Define the data contract
Each record should contain the fields the template needs, with a known meaning and safe fallback behavior. For example, a location carousel might use:
| Field | Purpose | Rendering guidance |
|---|---|---|
| title | Names the slide | Use as a heading; define a fallback for missing values. |
| address | Provides supporting detail | Omit the paragraph when empty. |
| image | Illustrates the record | Use a stable, valid URL and a useful alt value. |
| imageAlt | Describes informative image content | Use empty alt text for a purely decorative image. |
| url | Links to the record | Validate or generate it from trusted routing data. |
| sortOrder | Keeps ordering predictable | Apply ordering in the data query. |
Set limits and ordering in the query rather than relying on incidental database order. Decide whether unpublished, hidden, or incomplete records are excluded. If your data comes from users, escape output for the context where it appears; HTML text, an attribute, a URL, and JavaScript each have different escaping needs. Do not interpolate untrusted values into script source.
3. Query records and render the reusable template
Keep data access and presentation together in the platform’s reusable component or web-template system. The Microsoft Power Pages example uses a table with name, address, and image columns, retrieves rows with FetchXML, and binds values with Liquid. It also exposes configuration such as interval, count, and height in a component manifest. These are Power Pages details, not universal template syntax; translate the same structure to your framework’s query and binding language. See the [Microsoft Power Pages carousel tutorial](https://learn.microsoft.com/en-us/power-pages/configure/create-template) for its platform workflow.

A useful template contract exposes only settings that callers need. A record limit and a height are usually presentation choices; an autoplay interval is also a behavior and accessibility choice. Give optional settings safe defaults, and validate numeric values before putting them into attributes or styles. Render an empty state when the query returns no rows rather than outputting an empty carousel shell.
Here is the target markup shape. The loop syntax is illustrative pseudocode, not a framework-specific template language:
<section aria-roledescription="carousel" aria-labelledby="places-title">
<h2 id="places-title">Featured places</h2>
<div class="carousel-controls">
<button type="button" data-carousel-prev>Previous slide</button>
<button type="button" data-carousel-next>Next slide</button>
</div>
<ul class="carousel-slides" data-carousel-slides>
{% for record in records %}
<li data-carousel-slide>
<article>
<h3><a href="{{ record.url | escape }}">{{ record.title | escape }}</a></h3>
{% if record.image %}
<img src="{{ record.image | escape }}" alt="{{ record.imageAlt | escape }}">
{% endif %}
{% if record.address %}<p>{{ record.address | escape }}</p>{% endif %}
</article>
</li>
{% endfor %}
</ul>
<p data-carousel-status class="visually-hidden" aria-live="polite"></p>
</section>
Use your template engine’s documented escaping and conditional syntax. The sample shows the intended relationship between records and slides, not copy-paste-ready Liquid filters for every field type. Preserve a meaningful heading and list structure. W3C’s carousel guidance uses a labeled section and list items for slides, with meaningful content inside each item. See [W3C’s carousel tutorial](https://www.w3.org/WAI/tutorials/carousels/).
4. Add controls, keyboard behavior, and announcements
Controls must be actual buttons so they work with keyboard and assistive technology by default. Make their names clear, keep them in a predictable location, and avoid placing focusable controls inside slides that you hide with CSS while leaving them in the tab order. When a slide changes, tell users which item is active through a polite live region or an equivalent status mechanism. W3C recommends keyboard operation for all functionality and an announcement path for changes. See [W3C carousel functionality](https://www.w3.org/WAI/tutorials/carousels/functionality/).

The following small implementation expects the rendered list items and buttons shown above. It displays one slide at a time, wraps around at either end, updates the status, and avoids moving keyboard focus when navigation occurs:
const root = document.querySelector('[aria-roledescription="carousel"]');
if (root) {
const slides = [...root.querySelectorAll('[data-carousel-slide]')];
const status = root.querySelector('[data-carousel-status]');
let active = 0;
function show(index) {
if (!slides.length) return;
active = (index + slides.length) % slides.length;
slides.forEach((slide, i) => {
const current = i === active;
slide.hidden = !current;
slide.setAttribute('aria-current', current ? 'true' : 'false');
});
if (status) status.textContent = `Slide ${active + 1} of ${slides.length}`;
}
root.querySelector('[data-carousel-prev]')?.addEventListener('click', () => show(active - 1));
root.querySelector('[data-carousel-next]')?.addEventListener('click', () => show(active + 1));
show(0);
}
This implementation assumes that there is at least one slide and intentionally leaves an empty data set without active controls. In production, render controls only when there are multiple slides, or disable them clearly for a single slide. Use CSS to create a stable layout, and test the hidden-slide behavior with the actual links and interactive content in your cards. Do not use visual clipping as a substitute for removing inactive slides from interaction.
5. Treat autoplay as an opt-in feature
Manual navigation is simpler and gives readers control. If rotation is required, provide a clearly labeled start/stop button. Stop rotation when keyboard focus enters the carousel, stop while the pointer hovers over it, and let users pause movement. Respect the user’s reduced-motion preference for animated transitions. W3C notes that moving content can be too fast or distracting and says users must be able to pause carousel movement. See [W3C carousel animation guidance](https://www.w3.org/WAI/tutorials/carousels/animations/).
Do not add an interval just because a component exposes one. If you do add one, avoid restarting automatically after a user has stopped it. Keep the current item in the status announcement when a timer advances, and do not make a transition so fast that the content cannot be read. A basic reduced-motion rule for transitions is:
@media (prefers-reduced-motion: reduce) {
.carousel-slides,
.carousel-slides * {
scroll-behavior: auto;
animation-duration: 0.01ms;
transition-duration: 0.01ms;
}
}
That CSS only changes motion; it does not implement pause controls or focus-aware stopping. Those behaviors need JavaScript if the carousel rotates.
6. Choose the rendering and behavior layer
| Approach | Good fit | Trade-offs |
|---|---|---|
| CMS or server template | Records already live in a CMS or database and can be rendered on the server. | Template and query syntax are platform-specific. You still own accessible interaction. |
| Bootstrap carousel | The project already uses a compatible Bootstrap version and wants its JS component, controls, indicators, and touch options. | It depends on JavaScript, and slide dimensions need explicit styling. Verify version-specific attributes before copying examples. |
| CSS Overflow carousel features | Target browsers support browser-created scroll buttons and markers, with a no-JavaScript preference. | These are newer features. Provide a fallback and verify support for your audience. |
Bootstrap 5.1 documents controls, indicators, captions, per-item intervals, and touch behavior. A slide must be marked active, and Bootstrap does not normalize slide dimensions for you. Check the project version against the [Bootstrap 5.1 carousel documentation](https://getbootstrap.com/docs/5.1/components/carousel/) before adopting its data attributes or APIs.
CSS Overflow 5 describes browser-created carousel affordances: scroll buttons and markers on a scroll area. This can support touch, mouse, and keyboard interaction without JavaScript in browsers that implement the features. Browser support is recent and must be checked; Chrome documents availability from version 135. Keep a fallback, such as a horizontally scrollable list with visible controls, for browsers without support. See [Chrome for Developers’ CSS carousel guide](https://developer.chrome.com/blog/carousels-with-css/) and [CSS Overflow Module Level 5](https://drafts.csswg.org/css-overflow-5/).
7. Make the layout responsive and stable
Choose whether the design shows one item or a row of cards at each viewport size. Give images an aspect ratio or fixed frame so records with differently shaped images do not cause layout jumps. Use object-fit: cover when cropping is acceptable and choose image positioning deliberately. Set a minimum height only when it improves consistency without creating excessive blank space on small screens.
For a one-slide-at-a-time carousel, the list can use normal flow while inactive items are hidden. For a multi-card carousel, a horizontal overflow region may be more natural than paging one item at a time. Do not shrink text until it is hard to read just to fit another card. Ensure controls remain visible at narrow widths and have comfortable target size. Test long titles, missing images, translated text, zoom, and a single record.
8. Test the component with real data
- Render zero, one, and several records; confirm the empty and single-item states.
- Check that the query has deterministic ordering and respects its record limit.
- Navigate with keyboard only. Confirm buttons receive focus, activate with Enter or Space, and do not strand focus in hidden slides.
- Use a screen reader to verify the carousel label, slide content, button names, and change announcement.
- Check that images have useful alternative text, or empty alt when decorative.
- Test narrow and wide viewports, long content, slow image loads, and broken image URLs.
- If autoplay exists, verify stop/start, focus and hover behavior, and reduced-motion handling.
- Check the actual supported browsers before relying on newer CSS carousel features.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| No slides render | The query returns no rows, uses the wrong table name, or filters all records. | Inspect the query result, confirm the logical table name and permissions, and render an explicit empty state. |
| Slides appear in a random order | The query has no explicit sort. | Sort by a stable field such as sort order and then a unique identifier. |
| Every slide is visible | The active state is missing or the component styles are not loaded. | Initialize one active slide and verify the expected CSS and JavaScript are present. |
| Controls do nothing | Initialization ran before markup existed, selectors do not match, or a JavaScript error stopped execution. | Load the script after the component or initialize on DOM ready; inspect the console and selector values. |
| Screen reader does not report changes | The live region is absent, not updated, or updates too rapidly. | Keep a persistent polite status region and update it once per completed slide change. |
| Focus disappears after navigation | The focused element was inside a slide that was hidden. | Keep controls outside slides and move focus deliberately only when necessary; do not leave focus in hidden content. |
| Cards jump in height | Images and text have inconsistent intrinsic dimensions. | Constrain the image frame, reserve its aspect ratio, and choose a consistent card layout. |
| CSS carousel affordances are missing | The browser does not support the newer Overflow features. | Use the fallback scrollable list or a JavaScript-backed implementation for those browsers. |
10. Performance, reliability, and cost
Render only the fields and records needed for the component. Put filtering, ordering, and limiting in the data query when possible, and avoid repeating an expensive query for each slide. Lazy-load offscreen images when the platform and design support it, but make sure the initial active slide does not appear blank while its image loads. Reserve image dimensions to reduce layout shift. Keep event handlers scoped to the component so multiple carousels do not operate on one another.
For reliability, treat absent fields and failed image loads as normal data conditions. The title should still make sense without an image; links should be valid; and the component should remain navigable if there is only one record. If content is server-rendered, it can be present before the interaction script initializes, but ensure the initial markup does not expose a confusing pile of slides before JavaScript sets the active state. If scripts fail, a readable list is a useful fallback.
A local template carousel has no per-capture charge. Its costs are the application’s data access, rendering, image delivery, and maintenance of the interaction. If you also need screenshots of a page or rendered carousel for previews, documentation, or an automated workflow, ScreenshotNeo offers a website screenshot API and MCP server. The API has a free allowance and paid plans; see its [plans and product details](https://screenshotneo.com) before estimating usage.
Or skip the browser setup
If the goal is to capture the rendered carousel page as an image or PDF, you can call ScreenshotNeo directly instead of setting up a browser automation stack. See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) 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 the example URL with your publicly reachable page. ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before a capture. Bot checks, blank pages, and failed loads are not billed. Its MCP server lets AI agents use screenshot tools, and 1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. Those plans are listed at [ScreenshotNeo](https://screenshotneo.com). Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Should every record become a slide?
Usually not when the query can return an unbounded collection. Set a useful limit or choose a grid/list so people can browse the full set.
Do indicators replace previous and next buttons?
No. Indicators can supplement navigation, but provide clear buttons and ensure every navigation action is keyboard-operable.
Can I use CSS alone?
Yes, if a scrollable row meets the design need. The newer browser-created carousel controls need support in the target browsers, so keep a fallback.
What should happen when there is one slide?
Show the content and omit or disable navigation controls. There is no reason to announce or rotate a single item.


