Creating responsive web applications that work seamlessly across all devices is crucial in today’s mobile-first world. This comprehensive guide will walk you through practical techniques and best practices for building responsive Svelte applications.
Understanding Mobile-First Design
Mobile-first design means starting your development process with mobile devices in mind, then progressively enhancing the experience for larger screens. This approach ensures:
- Better performance on mobile devices
- Focused content prioritization
- Streamlined user experience
- Easier maintenance
1. Setting Up the Foundation
First, ensure your project has the proper viewport meta tag and base styles:
<!-- In your app.html -->
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="stylesheet" href="global.css">
</head>
/* global.css */
:root {
--spacing-unit: 1rem;
--max-width: 1200px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, sans-serif;
line-height: 1.5;
font-size: 16px;
}
2. Responsive Components with Svelte
Here’s an example of a responsive navigation component:
<!-- Nav.svelte -->
<script>
let isMenuOpen = false;
let windowWidth = 0;
// Reactive statement to handle resize
$: isMobile = windowWidth < 768;
</script>
<svelte:window bind:innerWidth={windowWidth}/>
<nav class:mobile={isMobile}>
<div class="logo">Brand</div>
{#if isMobile}
<button on:click={() => isMenuOpen = !isMenuOpen}>
Menu
</button>
{/if}
<ul class:open={isMenuOpen || !isMobile}>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<style>
nav {
padding: var(--spacing-unit);
display: flex;
justify-content: space-between;
align-items: center;
}
.mobile ul {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
}
.mobile ul.open {
display: block;
}
@media (min-width: 768px) {
ul {
display: flex;
gap: 2rem;
}
}
</style>
3. Advanced Grid Layouts
Here’s a responsive grid component that adapts to different screen sizes:
<!-- Grid.svelte -->
<script>
export let items = [];
export let minWidth = '300px';
</script>
<div class="grid" style="--min-width: {minWidth}">
{#each items as item}
<div class="grid-item">
<slot {item}>
<!-- Default content -->
<h3>{item.title}</h3>
<p>{item.description}</p>
</slot>
</div>
{/each}
</div>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(var(--min-width), 1fr));
gap: var(--spacing-unit);
padding: var(--spacing-unit);
}
.grid-item {
background: #f5f5f5;
padding: var(--spacing-unit);
border-radius: 4px;
}
</style>
4. Responsive Images with Art Direction
For more control over image presentation across different devices:
<!-- ResponsiveImage.svelte -->
<script>
export let images = {
mobile: '/images/mobile.jpg',
tablet: '/images/tablet.jpg',
desktop: '/images/desktop.jpg'
};
export let alt = '';
</script>
<picture>
<source
media="(min-width: 1024px)"
srcset={images.desktop}
>
<source
media="(min-width: 768px)"
srcset={images.tablet}
>
<img
src={images.mobile}
{alt}
loading="lazy"
>
</picture>
<style>
img {
width: 100%;
height: auto;
display: block;
}
</style>
5. CSS Custom Properties for Responsive Values
Use CSS custom properties to manage responsive values:
:root {
/* Base sizes */
--font-size-base: 1rem;
--spacing-base: 1rem;
/* Component-specific */
--header-height: 60px;
--sidebar-width: 100%;
}
@media (min-width: 768px) {
:root {
--font-size-base: 1.125rem;
--spacing-base: 1.5rem;
--sidebar-width: 300px;
}
}
@media (min-width: 1024px) {
:root {
--font-size-base: 1.25rem;
--spacing-base: 2rem;
--sidebar-width: 400px;
}
}
6. Testing Responsive Designs
Remember to test your responsive designs across different devices and browsers. Here are some key breakpoints to consider:
- Mobile: < 768px
- Tablet: 768px - 1023px
- Desktop: 1024px - 1279px
- Large Desktop: ≥ 1280px
Real-World Examples
Let’s look at some common UI patterns and how to implement them responsively in Svelte.
1. Responsive Card Layout with Container Queries
Container queries allow components to adapt based on their container’s size rather than the viewport:
<!-- ProductCard.svelte -->
<script>
export let product = {
title: '',
description: '',
price: 0,
image: ''
};
</script>
<div class="card-container">
<article class="product-card">
<img src={product.image} alt={product.title} />
<div class="content">
<h3>{product.title}</h3>
<p>{product.description}</p>
<span class="price">${product.price}</span>
</div>
</article>
</div>
<style>
.card-container {
container-type: inline-size;
container-name: card;
}
.product-card {
display: grid;
gap: 1rem;
}
@container card (min-width: 400px) {
.product-card {
grid-template-columns: 200px 1fr;
}
}
img {
width: 100%;
height: auto;
aspect-ratio: 1;
object-fit: cover;
}
</style>
2. Advanced Navigation Patterns
Here’s a more sophisticated navigation component with dropdown menus and responsive behaviors:
<!-- MainNav.svelte -->
<script>
import { fade, slide } from 'svelte/transition';
const menuItems = [
{
title: 'Products',
submenu: ['Laptops', 'Phones', 'Accessories']
},
{
title: 'Services',
submenu: ['Consulting', 'Support', 'Training']
}
];
let activeDropdown = null;
let isMenuOpen = false;
</script>
<nav class="main-nav">
<div class="nav-content">
<button class="mobile-toggle"
on:click={() => isMenuOpen = !isMenuOpen}
aria-expanded={isMenuOpen}>
<span class="sr-only">Menu</span>
<svg><!-- Menu icon --></svg>
</button>
<div class="nav-items" class:open={isMenuOpen}>
{#each menuItems as item}
<div class="nav-item">
<button
on:click={() => activeDropdown = activeDropdown === item ? null : item}
aria-expanded={activeDropdown === item}>
{item.title}
</button>
{#if activeDropdown === item}
<div class="dropdown" transition:slide>
{#each item.submenu as subItem}
<a href="#{subItem.toLowerCase()}">{subItem}</a>
{/each}
</div>
{/if}
</div>
{/each}
</div>
</div>
</nav>
<style>
.main-nav {
position: sticky;
top: 0;
background: var(--nav-bg, #fff);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.nav-content {
max-width: var(--max-width);
margin: 0 auto;
padding: 1rem;
}
.mobile-toggle {
display: none;
}
@media (max-width: 768px) {
.mobile-toggle {
display: block;
}
.nav-items {
display: none;
}
.nav-items.open {
display: block;
}
}
/* Additional styles... */
</style>
3. Dynamic Layout Switcher
A component that changes layout based on screen size and content:
<!-- DynamicLayout.svelte -->
<script>
import { onMount } from 'svelte';
export let items = [];
export let breakpoint = 768;
let layoutType = 'stack';
function updateLayout() {
layoutType = window.innerWidth < breakpoint ? 'stack' : 'grid';
}
onMount(() => {
updateLayout();
window.addEventListener('resize', updateLayout);
return () => window.removeEventListener('resize', updateLayout);
});
</script>
<div class="layout" class:stack={layoutType === 'stack'} class:grid={layoutType === 'grid'}>
{#each items as item}
<div class="item">
<slot {item} />
</div>
{/each}
</div>
<style>
.layout {
--gap: 1rem;
display: flex;
flex-wrap: wrap;
gap: var(--gap);
}
.stack {
flex-direction: column;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.item {
background: var(--item-bg, #f5f5f5);
padding: var(--gap);
border-radius: 4px;
}
</style>
Advanced Responsive Techniques
1. Fluid Typography
Create smooth font scaling across different viewport sizes:
:root {
--fluid-min-width: 320;
--fluid-max-width: 1200;
--fluid-min-size: 16;
--fluid-max-size: 24;
--fluid-size: calc(
(var(--fluid-min-size) * 1px) + (var(--fluid-max-size) - var(--fluid-min-size)) *
(100vw - (var(--fluid-min-width) * 1px)) /
(var(--fluid-max-width) - var(--fluid-min-width))
);
}
body {
font-size: var(--fluid-size);
}
2. Content-Aware Components
Example of a component that adapts based on content length:
<!-- AdaptiveCard.svelte -->
<script>
import { onMount } from 'svelte';
export let content = '';
let contentElement;
let isLongContent = false;
onMount(() => {
const observer = new ResizeObserver(entries => {
for (let entry of entries) {
isLongContent = entry.target.scrollHeight > 200;
}
});
observer.observe(contentElement);
return () => observer.disconnect();
});
</script>
<div class="card" class:long={isLongContent}>
<div class="content" bind:this={contentElement}>
{content}
</div>
{#if isLongContent}
<button class="expand">Read More</button>
{/if}
</div>
<style>
.card {
max-height: 200px;
overflow: hidden;
position: relative;
}
.card.long::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 50px;
background: linear-gradient(transparent, white);
}
</style>
Best Practices
- Always start with mobile layouts first
- Use relative units (rem, em, %) instead of fixed pixels
- Test on real devices when possible
- Consider touch targets (minimum 44x44px)
- Optimize images and assets for different screen sizes
- Use CSS Grid and Flexbox for layouts
- Implement progressive enhancement
Performance Optimization Tips
- Use
loading="lazy"
for images - Implement responsive image srcsets
- Critical CSS inlining
- Efficient media query breakpoints
- Debounce resize handlers
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// Usage in Svelte component
const debouncedResize = debounce(() => {
// handle resize
}, 250);
onMount(() => {
window.addEventListener('resize', debouncedResize);
return () => window.removeEventListener('resize', debouncedResize);
});
Conclusion
Building responsive Svelte applications requires careful planning and implementation. By following these techniques and best practices, you can create web applications that provide an excellent user experience across all devices.
Remember that responsive design is not just about making things fit on different screens—it’s about creating an optimal experience for users regardless of their device or context.