1184 words
6 minutes
Responsive Web Design in Svelte: Techniques for Mobile-First Projects

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#

  1. Always start with mobile layouts first
  2. Use relative units (rem, em, %) instead of fixed pixels
  3. Test on real devices when possible
  4. Consider touch targets (minimum 44x44px)
  5. Optimize images and assets for different screen sizes
  6. Use CSS Grid and Flexbox for layouts
  7. Implement progressive enhancement

Performance Optimization Tips#

  1. Use loading="lazy" for images
  2. Implement responsive image srcsets
  3. Critical CSS inlining
  4. Efficient media query breakpoints
  5. 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.

Resources#

Responsive Web Design in Svelte: Techniques for Mobile-First Projects
https://zxce3.net/posts/responsive-web-design-in-svelte-techniques-for-mobile-first-projects/
Author
Memet Zx
Published at
2024-05-20