@manufosela/convert2webp

@manufosela/convert2webp

Image to WebP conversion utility using Canvas API

convert2webp

Image to WebP conversion utility using Canvas API

import { convertToWebP } from '@manufosela/convert2webp';

const result = await convertToWebP(file, 0.8);

Drop images here or click to select

Supports PNG, JPEG, GIF, BMP

80%
Demo code (CodePen-ready HTML, CSS, JS)
HTML (html)
<div class="top-links">
    <a href="https://manufosela.dev/utils/">Utils catalog</a>
    <a href="https://manufosela.dev/">Main catalog</a>
    <a href="https://github.com/manufosela/utils/tree/main/packages/convert2webp">Source</a>
  </div>
        <demo-theme-toggle></demo-theme-toggle>
  <h1>convert2webp</h1>
  <p class="subtitle">Image to WebP conversion utility using Canvas API</p>

  <div class="options" style="margin-top: 0;">
    <div class="option-group">
      <label>Quick usage</label>
      <pre class="code-block"><code>import { convertToWebP } from '@manufosela/convert2webp';

const result = await convertToWebP(file, 0.8);
</code></pre>
    </div>
  </div>

  <div class="upload-area" id="uploadArea">
    <p>Drop images here or click to select</p>
    <p style="font-size: 0.875rem; color: #999;">Supports PNG, JPEG, GIF, BMP</p>
    <input type="file" id="fileInput" accept="image/*" multiple>
  </div>

  <div class="options">
    <div class="option-group">
      <label for="quality">Quality (0-100%)</label>
      <input type="range" id="quality" min="1" max="100" value="80">
      <span id="qualityValue">80%</span>
    </div>
    <div class="option-group">
      <label for="maxWidth">Max Width (px)</label>
      <input type="number" id="maxWidth" placeholder="No limit" min="1">
    </div>
    <div class="option-group">
      <label for="maxHeight">Max Height (px)</label>
      <input type="number" id="maxHeight" placeholder="No limit" min="1">
    </div>
  </div>

  <div class="results" id="results"></div>
CSS (css)
:root {
  --bg: #0c0f14;
  --bg-elevated: #141923;
  --bg-panel: #171d28;
  --border: #262f3f;
  --text: #f4f6fb;
  --text-muted: #a7b0c2;
  --text-dim: #7d879b;
  --accent: #ff8a3d;
  --accent-strong: #ff6a00;
  --accent-soft: rgba(255, 138, 61, 0.16);
  --shadow: 0 20px 50px rgba(5, 8, 14, 0.45);
  --radius-lg: 22px;
  --radius-md: 14px;
  --radius-sm: 10px;
  --max-width: 1160px;
}

* {
      box-sizing: border-box;
    }

    h1 {
      color: var(--accent);
      margin-bottom: 0.5rem;
    }

    .subtitle {
      color: var(--text-muted);
      margin-bottom: 2rem;
    }

    .upload-area {
      border: 2px dashed var(--border);
      border-radius: 8px;
      padding: 2rem;
      text-align: center;
      background: var(--bg-elevated);
      transition: border-color 0.2s, background-color 0.2s;
      cursor: pointer;
    }

    .upload-area:hover,
    .upload-area.dragover {
      border-color: var(--accent);
      background: #f8f9ff;
    }

    .upload-area input {
      display: none;
    }

    .options {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 1rem;
      margin: 1.5rem 0;
      padding: 1rem;
      background: var(--bg-elevated);
      border-radius: 8px;
    }

    .option-group {
      display: flex;
      flex-direction: column;
      gap: 0.5rem;
    }

    .option-group label {
      font-weight: 500;
      color: var(--text-muted);
    }

    .option-group input {
      padding: 0.5rem;
      border: 1px solid var(--border);
      border-radius: 4px;
    }

    .results {
      margin-top: 2rem;
    }

    .result-card {
      background: var(--bg-elevated);
      border-radius: 8px;
      padding: 1rem;
      margin-bottom: 1rem;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }

    .result-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 1rem;
      padding-bottom: 0.5rem;
      border-bottom: 1px solid var(--border);
    }

    .result-images {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }

    .image-container {
      text-align: center;
    }

    .image-container img {
      max-width: 100%;
      max-height: 200px;
      border-radius: 4px;
      border: 1px solid var(--border);
    }

    .image-container p {
      margin: 0.5rem 0 0;
      font-size: 0.875rem;
      color: var(--text-muted);
    }

    .top-links {
      display: flex;
      gap: 0.5rem;
      flex-wrap: wrap;
      margin-bottom: 1rem;
    }

    .top-links a {
      font-size: 0.8rem;
      color: var(--text-muted);
      text-decoration: none;
      border: 1px solid var(--border);
      padding: 0.35rem 0.75rem;
      border-radius: 999px;
    }

    .stats {
      display: flex;
      gap: 2rem;
      margin-top: 1rem;
      padding-top: 1rem;
      border-top: 1px solid var(--border);
    }

    .stat {
      text-align: center;
    }

    .stat-value {
      font-size: 1.5rem;
      font-weight: bold;
      color: var(--text);
    }

    .stat-label {
      font-size: 0.875rem;
      color: var(--text-muted);
    }

    .savings {
      color: #28a745;
    }

    button {
      padding: 0.5rem 1rem;
      background: var(--accent);
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      font-size: 0.875rem;
    }

    button:hover {
      background: var(--accent-strong);
    }

    .download-btn {
      background: #28a745;
    }

    .download-btn:hover {
      background: #1e7e34;
    }

    .code-block {
      background: #0b0f1a;
      border: 1px solid #2b3247;
      border-radius: 8px;
      padding: 12px;
      color: #d6d9e6;
      font-family: "JetBrains Mono", "Courier New", monospace;
      font-size: 0.85rem;
      white-space: pre-wrap;
      word-break: break-word;
    }
  






:root {
  --bg: #0f1117;
  --bg-2: #151a26;
  --bg-spot-1: #1a2136;
  --bg-spot-2: #1d1b34;
  --card: #1c2233;
  --text: #f3f6ff;
  --muted: #b8c0d9;
  --line: #2b3247;
  --accent: #ffb000;
  --accent-2: #00d0ff;
  --surface: #0b0f1a;
  --code-bg: #0b0f1a;
  --code-text: #d6d9e6;
}

:root.light {
  --bg: #f5f5f7;
  --bg-2: #ffffff;
  --bg-spot-1: #f8e9d0;
  --bg-spot-2: #e8eef8;
  --card: #ffffff;
  --text: #1d1d1f;
  --muted: #6b7280;
  --line: #e5e7eb;
  --accent: #ffb000;
  --accent-2: #00a7d6;
  --surface: #f3f4f6;
  --code-bg: #111827;
  --code-text: #f9fafb;
}

:root.dark {
  --bg: #0f1117;
  --bg-2: #151a26;
  --bg-spot-1: #1a2136;
  --bg-spot-2: #1d1b34;
  --card: #1c2233;
  --text: #f3f6ff;
  --muted: #b8c0d9;
  --line: #2b3247;
  --accent: #ffb000;
  --accent-2: #00d0ff;
  --surface: #0b0f1a;
  --code-bg: #0b0f1a;
  --code-text: #d6d9e6;
}

h1 {
  color: var(--accent) !important;
}

.top-links a,
.topbar a {
  color: var(--muted) !important;
  border-color: var(--line) !important;
}

.top-links a:hover,
.topbar a:hover {
  color: var(--text) !important;
}

.card,
.section,
.demo-section,
.panel {
  background: var(--card) !important;
  border-color: var(--line) !important;
  color: var(--text) !important;
}

.code-block,
pre,
code,
.output,
.current-url,
.event-log,
.result-card,
.log {
  background: var(--code-bg) !important;
  color: var(--code-text) !important;
  border-color: var(--line) !important;
}

input,
select,
textarea {
  background: var(--surface) !important;
  color: var(--text) !important;
  border-color: var(--line) !important;
}

button {
  background: linear-gradient(120deg, var(--accent), #ff6a00) !important;
  color: #111 !important;
}

footer {
  color: var(--muted) !important;
}

footer a {
  color: var(--text) !important;
}
JS (js)
import {
      convertToWebP,
      isWebPSupported,
      createDownloadUrl
    } from "https://esm.sh/@manufosela/convert2webp";

    const uploadArea = document.getElementById('uploadArea');
    const fileInput = document.getElementById('fileInput');
    const qualityInput = document.getElementById('quality');
    const qualityValue = document.getElementById('qualityValue');
    const maxWidthInput = document.getElementById('maxWidth');
    const maxHeightInput = document.getElementById('maxHeight');
    const results = document.getElementById('results');

    // Check WebP support
    isWebPSupported().then(supported => {
      if (!supported) {
        alert('Your browser does not support WebP conversion');
      }
    });

    // Quality slider
    qualityInput.addEventListener('input', () => {
      qualityValue.textContent = `${qualityInput.value}%`;
    });

    // Drag and drop
    uploadArea.addEventListener('click', () => fileInput.click());

    uploadArea.addEventListener('dragover', (e) => {
      e.preventDefault();
      uploadArea.classList.add('dragover');
    });

    uploadArea.addEventListener('dragleave', () => {
      uploadArea.classList.remove('dragover');
    });

    uploadArea.addEventListener('drop', (e) => {
      e.preventDefault();
      uploadArea.classList.remove('dragover');
      handleFiles(e.dataTransfer.files);
    });

    fileInput.addEventListener('change', (e) => {
      handleFiles(e.target.files);
    });

    async function handleFiles(files) {
      const options = {
        quality: qualityInput.value / 100,
        maxWidth: maxWidthInput.value ? parseInt(maxWidthInput.value) : undefined,
        maxHeight: maxHeightInput.value ? parseInt(maxHeightInput.value) : undefined
      };

      for (const file of files) {
        if (!file.type.startsWith('image/')) {
          continue;
        }

        try {
          const result = await convertToWebP(file, options);
          displayResult(file, result);
        } catch (error) {
          console.error('Conversion failed:', error);
          alert(`Failed to convert ${file.name}: ${error.message}`);
        }
      }
    }

    function formatBytes(bytes) {
      if (bytes < 1024) return `${bytes} B`;
      if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
      return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
    }

    function displayResult(originalFile, result) {
      const originalUrl = URL.createObjectURL(originalFile);
      const webpUrl = createDownloadUrl(result.blob);

      const savings = ((1 - result.size / originalFile.size) * 100).toFixed(1);

      const card = document.createElement('div');
      card.className = 'result-card';
      card.innerHTML = `
        <div class="result-header">
          <strong>${originalFile.name}</strong>
          <button class="download-btn" onclick="downloadFile('${webpUrl}', '${originalFile.name.replace(/\.[^.]+$/, '.webp')}')">
            Download WebP
          </button>
        </div>
        <div class="result-images">
          <div class="image-container">
            <img src="${originalUrl}" alt="Original">
            <p>Original (${formatBytes(originalFile.size)})</p>
          </div>
          <div class="image-container">
            <img src="${webpUrl}" alt="WebP">
            <p>WebP (${formatBytes(result.size)})</p>
          </div>
        </div>
        <div class="stats">
          <div class="stat">
            <div class="stat-value">${result.width} x ${result.height}</div>
            <div class="stat-label">Dimensions</div>
          </div>
          <div class="stat">
            <div class="stat-value savings">${savings}%</div>
            <div class="stat-label">Size Reduction</div>
          </div>
          <div class="stat">
            <div class="stat-value">${(qualityInput.value)}%</div>
            <div class="stat-label">Quality</div>
          </div>
        </div>
      `;

      results.insertBefore(card, results.firstChild);
    }

    // Global download function
    window.downloadFile = function(url, filename) {
      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.click();
    };
  

class DemoThemeToggle extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.handleClick = this.handleClick.bind(this);
  }

  connectedCallback() {
    const saved = localStorage.getItem('utils-demo-theme');
    document.documentElement.classList.remove('dark');
    document.documentElement.classList.toggle('light', saved === 'light');

    this.render();
  }

  render() {
    const isLight = document.documentElement.classList.contains('light');
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: inline-flex;
        }
        .toggle {
          display: inline-flex;
          align-items: center;
          border: 1px solid var(--line);
          border-radius: 999px;
          overflow: hidden;
          background: var(--surface);
        }
        button {
          border: none;
          background: transparent;
          color: var(--muted);
          padding: 6px 12px;
          font-size: 0.8rem;
          cursor: pointer;
          font-family: "Space Grotesk", "Trebuchet MS", Arial, sans-serif;
        }
        button.active {
          background: linear-gradient(120deg, var(--accent), #ff6a00);
          color: #111;
          font-weight: 600;
        }
      </style>
      <div class="toggle" role="group" aria-label="Theme toggle">
        <button class="${isLight ? 'active' : ''}" data-theme="light">Light</button>
        <button class="${isLight ? '' : 'active'}" data-theme="dark">Dark</button>
      </div>
    `;

    this.shadowRoot.querySelectorAll('button').forEach((btn) => {
      btn.addEventListener('click', this.handleClick);
    });
  }

  handleClick(event) {
    const theme = event.currentTarget.dataset.theme;
    const isLight = theme === 'light';
    document.documentElement.classList.toggle('light', isLight);
    document.documentElement.classList.remove('dark');
    localStorage.setItem('utils-demo-theme', isLight ? 'light' : 'dark');
    this.render();
  }
}

customElements.define('demo-theme-toggle', DemoThemeToggle);