Svelte

Build for Cloud Phone with Svelte

Svelte is a popular JavaScript UI framework that uses a compiler to write reactive components. Learn how to build a Cloud Phone widget using Svelte, or skip ahead to the finished sample code and live demo hosted on GitHub Pages.

Need a primer on Cloud Phone?

Getting started with Cloud Phone

Learn how Cloud Phone brings compelling web apps to the next billion users

Setup

Prerequisites

Dependency Versions

This sample was developed using the following libraries and frameworks.

DependencyVersion
Nodev20.11.0 (LTS)
npm10.9.0
Sveltev5
Svelte CLI0.6.10
SvelteKit2.15.1
adapter-static3
sveltekit-i18n24
Vite6

Svelte CLI Setup

Use sv, SvelteKit’s command line interface (CLI) to scaffold a new SvelteKit project.

Scaffolding with Svelte CLI
npx sv create cloudphone-svelte-sample

You can also use . for the project name to scaffold in the current directory.

Then follow the prompts to create a new SvelteKit project using npm (or your preferred package manager) to install dependencies into the node_modules/ directory.

┌  Welcome to the Svelte CLI! (v0.6.10)
◇  Which template would you like?
│  SvelteKit minimal
◇  Add type checking with Typescript?
│  No
◆  Project created
◇  What would you like to add to your project? (use arrow keys / space bar)
│  none
◇  Which package manager do you want to install dependencies with?
│  npm
◆  Successfully installed dependencies
◇  Project next steps ─────────────────────────────────────────────────────╮
│                                                                          │
│  1: cd cloudphone-svelte-sample                                          │
│  2: git init && git add -A && git commit -m "Initial commit" (optional)  │
│  3: npm run dev -- --open                                                │
│                                                                          │
├──────────────────────────────────────────────────────────────────────────╯
└  You're all set!

The command npm run dev is aliased in the scripts section of the package.json file to run vite dev. This command starts a local HTTP server on port 5173 (by default), available at http://localhost:5173/

package.json
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "preview": "vite preview"
  }

Development

Routing

SvelteKit provides routing with both server side rendering (SSR) and client side rendering (CSR).

Install adapter-static using NPM for single-page application (SPA) routing:

npm i -D @sveltejs/adapter-static

Then in svelte.config.js, replace the @sveltejs/adapter-auto import with @sveltejs/adapter-static:

svelte.config.js
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter(),
  }
};

export default config;

Internationalization

sveltekit-i18n is a popular package for handling internationalization (abbreviated i18n) in Svelte apps.

npm install sveltekit-i18n

For this demo, store translations as JSON files located at src/lib/translations/<language_code>/*.json. Define a configuration like the one below.

translations/index.js
import i18n from 'sveltekit-i18n';

export const getLanguageName = (contextLanguage, languageCode) => {
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames
  return new Intl.DisplayNames([contextLanguage], {
    type: 'language'
  }).of(languageCode);
};

/** @type {import('sveltekit-i18n').Config} */
const config = ({
  translations: {
    en: {
      en: getLanguageName('en-US', 'en'),
      es: getLanguageName('en-US', 'es-MX'),
    },
    es: {
      en: getLanguageName('es-MX', 'en-US'),
      es: getLanguageName('es-MX', 'es-MX'),
    },
  },
  loaders: [
    {
      locale: 'en',
      key: 'common',
      loader: async () => (
        await import('./en/common.json')
      ).default,
    },
    {
      locale: 'es',
      key: 'common',
      loader: async () => (
        await import('./es/common.json')
      ).default,
    },
    {
      locale: 'en',
      key: 'about',
      loader: async () => (
        await import('./en/about.json')
      ).default,
    },
    {
      locale: 'es',
      key: 'about',
      loader: async () => (
        await import('./es/about.json')
      ).default,
    }
  ],
});

export const { t, locale, locales, loading, loadTranslations } = new i18n(config);

The demo includes two lasy-loaded translations–English and Spanish–and uses the Intl API to get the display name of each supported language. Translations are namespaced using the key property, prefixed during lookups.

<script>
  import { t } from '$lib/translations';
</script>

<h2>{$t('common.about')}</h2>

Automatically set the default language based on the navigator.language.

+layout.js
import { locale, locales, loadTranslations } from '$lib/translations/index';

const defaultLocale = 'en';

// This is a very naive approach. Consider using a library
// that doesn't drop country codes
function getDefaultLanguage(languageSet) {
  const preferredLanguage = (navigator.language || '').split('-')[0].toLowerCase();
  if (languageSet.has(preferredLanguage)) return preferredLanguage;
  return defaultLocale;
}

// Disable server side rendering (SSR)
export const ssr = false;

/** @type {import('@sveltejs/kit').Load} */
export const load = async () => {
  // Default to the user-selected locale, if set
  // Otherwise try to match the browser language, if available
  const userLanguage = locale.get();
  const initialLocale = (userLanguage) ? userLanguage : getDefaultLanguage(new Set(locales.get()));

  await loadTranslations(initialLocale);

  return {};
};

This sample ignores the country code when determining the default language. See next-language-detector for a better matching example.

Keyboard Navigation

Cloud Phones have directional pads (D-pads) to support keyboard navigation. Unlike desktop that uses the tabindex property and Tab (⇥) key to the next focusable element, Cloud Phone uses Arrow keys (↑↓←→).

OptionsMenu Screenshot
OptionsMenu
Settings Page Screenshot
Settings: Language Selection

Autofocus on the first element in a page, list, or grid to set the user’s bearings. Use the autofocus attribute to take advantage of the browser’s native autofocus capabilities.

settings/+page.svelte
<script>
  const autofocus = (element) => element?.focus();
</script>

<section id="app">
  <h1>Settings</h1>
  <button autofocus>Language</button>
</section>

However, the autofocus attribute will trigger a a11y_autofocus compiler warning that can be suppressed with the <!-- svelte-ignore a11y_autofocus --> comment. Alternatively, Svelte Actions are element-lifecycle functions declared by the use directive.

settings/+page.svelte
<script>
	const autofocus = (element) => element?.focus();
</script>

<section id="app">
  <h1>Settings</h1>
  <button use:autofocus>Language</button>
</section>

Extend the use directive approach to autofocus on the first item in a list.

findFirstFocusable.js
// CSS selector to find focusable elements
const FOCUSABLE_CSS_SELECTOR = `a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), *[tabindex], *[contenteditable]`;

// Find the first focusable element in a container (i.e. list)
function findFirstFocusable(container) {
  return container.querySelector(FOCUSABLE_CSS_SELECTOR);
}

// Auto-focus the first focusable item in element when its mounted
function autoFocusFirstFocusable(element) {
  if (element) {
    findFirstFocusable(element)?.focus();
  }
}

This technique is used in the OptionsMenu component. Here is a simplified example:

OptionsMenu.svelte
<script>
  import { autoFocusFirstFocusable } from "$lib/utils";

  const { items = [] } = $props();
</script>

<menu use:autoFocusFirstFocusable>
  {#each items as item, index}
    <a {...item}>{item.text}</a>
  {/each}
</menu>

The OptionsMenu component can have links as children, to create a navigation menu triggered by the Left Soft Key (LSK)

<OptionsMenu
  items={[{
    href: '/about',
    text: $t('common.about'),
  }, {
    href: '/settings',
    text: $t('common.settings'),
  }, {
    href: 'ttps://www.cloudfone.com/dev-privacy',
    target: '_self',
    text: $t('common.privacy')
  }]} />

Hosting on GitHub Pages

GitHub Pages provides free hosting for public, open-source repositories. This demo uses the github-pages-deploy-action GitHub Action to build and deploy HTML, CSS, and JS. The action is defined in pages.yml.

Update SvelteKit Config to use adapter-static to create a single page application (SPA) that’s compatible with GitHub Pages. Replace config.kit.paths.base with your GitHub repository name.

svelte.config.js
import adapter from '@sveltejs/adapter-static';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  kit: {
    adapter: adapter({
      fallback: 'index.html'
    }),
    paths: {
      base: ((process.env.NODE_ENV === 'production') ?
        '/cloudphone-svelte-sample' : ''),
    }
  }
};

export default config;

Use a two-step GitHub Action to build and deploy the SPA on pushes to the main branch. By default, adapter-static builds in the /build directory determined by the pages and assets configurations.

pages.yml
name: Deploy Pages
on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: "Checkout 🛎️"
        uses: actions/checkout@v2
      - name: "Setup Node 🚧"
        uses: actions/setup-node@v4
        with:
          node-version: ">=20.11.0"
          cache: npm
      - name: "Dependencies 🏗️"
        run: |
          npm ci
      - name: "Build 🔧"
        run: |
          npm run build
        env:
          BASE_PATH: "/${{ github.event.repository.name }}"
      - name: Upload Artifacts
        uses: actions/upload-pages-artifact@v3
        with:
          path: "build/"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      pages: write
      id-token: write
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: "Deploy 🚀"
        id: deployment
        uses: actions/deploy-pages@v4

Wrap Up

Tips

Take full advantage of the Svelte compiler optimizations and growing ecosystem with the following tips.

Static Site Generator

Use SvelteKit’s adapter-static to generate a static website, if your app is simple enough. Use the Accept-Language header to default users to their preferred language.

Component Library

Build faster with robust component libraries like Material UI, Skeleton, or Flowbite. Many libraries like Svelte Material UI (SMUI) are keyboard-accessible by default.

Testing

Develop reliably with testing utilities like Vitest and Playwright. Target Chromium in Playwright to most closely match Cloud Phone.

Svelte Tips

  • Use the Svelte VS Code integrated development environment (IDE) plugin
  • Segment your application into reusable components
  • Maintain an organized file structure
  • Use descriptive variable and function names
  • Move complex logic outside of components

Next Steps

Register for the Cloud Phone Developer Program to try the live demo on the Cloud Phone Simulator or a physical device. Fork us on GitHub to start coding your first Cloud Phone widget using Svelte and set up a custom domain name to point to GitHub Pages.

Contact Us

Reach out when you are ready to publish your first Cloud Phone widget

Get Support