home / skills / dchuk / claude-code-tauri-skills / tauri-splashscreen

tauri-splashscreen skill

/tauri/tauri-splashscreen

This skill guides you through adding a Tauri splashscreen and startup sequence, configuring windows, frontend and backend init, and styling for a polished

npx playbooks add skill dchuk/claude-code-tauri-skills --skill tauri-splashscreen

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
11.3 KB
---
name: adding-tauri-splashscreen
description: Guides the user through adding a Tauri splashscreen, splash screen, loading screen, or startup screen to their application. Covers configuration, custom splash HTML, closing the splash when ready, and styling.
---

# Tauri Splashscreen Implementation

This skill covers implementing splash screens in Tauri v2 applications. A splash screen displays during application startup while the main window loads and initializes.

## Overview

The splash screen pattern involves:
1. Showing a splash window immediately on launch
2. Hiding the main window until ready
3. Performing initialization tasks (frontend and backend)
4. Closing splash and showing main window when complete

## Configuration

### Window Configuration

Configure both windows in `tauri.conf.json`:

```json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "title": "My Application",
        "width": 1200,
        "height": 800,
        "visible": false
      },
      {
        "label": "splashscreen",
        "title": "Loading",
        "url": "splashscreen.html",
        "width": 400,
        "height": 300,
        "center": true,
        "resizable": false,
        "decorations": false,
        "transparent": true,
        "alwaysOnTop": true
      }
    ]
  }
}
```

Key settings:
- `"visible": false` on main window - hides it until ready
- `"url": "splashscreen.html"` - points to splash screen HTML
- `"decorations": false` - removes window chrome for cleaner look
- `"transparent": true` - enables transparent backgrounds
- `"alwaysOnTop": true` - keeps splash visible during loading

## Splash Screen HTML

Create `splashscreen.html` in your frontend source directory (e.g., `src/` or `public/`):

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Loading</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html, body {
      height: 100%;
      overflow: hidden;
      background: transparent;
    }

    .splash-container {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
      border-radius: 12px;
      color: white;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    }

    .logo {
      width: 80px;
      height: 80px;
      margin-bottom: 24px;
    }

    .app-name {
      font-size: 24px;
      font-weight: 600;
      margin-bottom: 16px;
    }

    .loading-spinner {
      width: 40px;
      height: 40px;
      border: 3px solid rgba(255, 255, 255, 0.3);
      border-top-color: #4f46e5;
      border-radius: 50%;
      animation: spin 1s linear infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    .loading-text {
      margin-top: 16px;
      font-size: 14px;
      color: rgba(255, 255, 255, 0.7);
    }
  </style>
</head>
<body>
  <div class="splash-container">
    <!-- Replace with your logo -->
    <svg class="logo" viewBox="0 0 100 100" fill="none">
      <circle cx="50" cy="50" r="45" stroke="#4f46e5" stroke-width="4"/>
      <path d="M30 50 L45 65 L70 35" stroke="#4f46e5" stroke-width="4" fill="none"/>
    </svg>
    <div class="app-name">My Application</div>
    <div class="loading-spinner"></div>
    <div class="loading-text">Loading...</div>
  </div>
</body>
</html>
```

## Frontend Setup

### TypeScript/JavaScript Implementation

In your main entry file (e.g., `src/main.ts`):

```typescript
import { invoke } from '@tauri-apps/api/core';

// Helper function for delays
function sleep(seconds: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, seconds * 1000));
}

// Frontend initialization
async function initializeFrontend(): Promise<void> {
  // Perform frontend setup tasks here:
  // - Load configuration
  // - Initialize state management
  // - Set up routing
  // - Preload critical assets

  // Example: simulate initialization time
  await sleep(1);

  // Notify backend that frontend is ready
  await invoke('set_complete', { task: 'frontend' });
}

// Start initialization when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
  initializeFrontend().catch(console.error);
});
```

### Alternative: Using Window Events

```typescript
import { invoke } from '@tauri-apps/api/core';
import { getCurrentWindow } from '@tauri-apps/api/window';

async function initializeFrontend(): Promise<void> {
  // Your initialization logic
  const config = await loadConfig();
  await setupRouter();
  await preloadAssets();

  // Signal completion
  await invoke('set_complete', { task: 'frontend' });
}

// Wait for window to be fully ready
getCurrentWindow().once('tauri://created', () => {
  initializeFrontend();
});
```

## Backend Setup

### Add Tokio Dependency

```bash
cargo add tokio --features time
```

### Rust Implementation

In `src-tauri/src/lib.rs`:

```rust
use std::sync::Mutex;
use tauri::{AppHandle, Manager, State};

// Track initialization state
struct SetupState {
    frontend_task: bool,
    backend_task: bool,
}

impl Default for SetupState {
    fn default() -> Self {
        Self {
            frontend_task: false,
            backend_task: false,
        }
    }
}

// Command to mark tasks complete
#[tauri::command]
async fn set_complete(
    app: AppHandle,
    state: State<'_, Mutex<SetupState>>,
    task: String,
) -> Result<(), String> {
    let mut state = state.lock().map_err(|e| e.to_string())?;

    match task.as_str() {
        "frontend" => state.frontend_task = true,
        "backend" => state.backend_task = true,
        _ => return Err(format!("Unknown task: {}", task)),
    }

    // Check if all tasks are complete
    if state.frontend_task && state.backend_task {
        // Close splash and show main window
        if let Some(splash) = app.get_webview_window("splashscreen") {
            splash.close().map_err(|e| e.to_string())?;
        }

        if let Some(main) = app.get_webview_window("main") {
            main.show().map_err(|e| e.to_string())?;
            main.set_focus().map_err(|e| e.to_string())?;
        }
    }

    Ok(())
}

// Backend initialization
async fn setup_backend(app: AppHandle) {
    // IMPORTANT: Use tokio::time::sleep, NOT std::thread::sleep
    // std::thread::sleep blocks the entire async runtime

    // Perform backend initialization:
    // - Database connections
    // - Load configuration
    // - Initialize services

    // Example: simulate work
    tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

    // Mark backend as complete
    if let Some(state) = app.try_state::<Mutex<SetupState>>() {
        let _ = set_complete(
            app.clone(),
            state,
            "backend".to_string(),
        ).await;
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .manage(Mutex::new(SetupState::default()))
        .invoke_handler(tauri::generate_handler![set_complete])
        .setup(|app| {
            let handle = app.handle().clone();

            // Spawn backend initialization
            tauri::async_runtime::spawn(async move {
                setup_backend(handle).await;
            });

            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

## Simple Implementation

For simpler cases where you only need to wait for the frontend:

### Configuration

```json
{
  "app": {
    "windows": [
      {
        "label": "main",
        "visible": false
      },
      {
        "label": "splashscreen",
        "url": "splashscreen.html",
        "width": 400,
        "height": 300,
        "decorations": false
      }
    ]
  }
}
```

### Frontend

```typescript
import { invoke } from '@tauri-apps/api/core';

async function init() {
  // Initialize your app
  await setupApp();

  // Close splash, show main
  await invoke('close_splashscreen');
}

document.addEventListener('DOMContentLoaded', init);
```

### Backend

```rust
use tauri::{AppHandle, Manager};

#[tauri::command]
async fn close_splashscreen(app: AppHandle) -> Result<(), String> {
    if let Some(splash) = app.get_webview_window("splashscreen") {
        splash.close().map_err(|e| e.to_string())?;
    }

    if let Some(main) = app.get_webview_window("main") {
        main.show().map_err(|e| e.to_string())?;
        main.set_focus().map_err(|e| e.to_string())?;
    }

    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![close_splashscreen])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
```

## Styling Variations

### Minimal Splash

```html
<style>
  .splash-container {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100vh;
    background: #ffffff;
  }

  .logo {
    width: 120px;
    animation: pulse 2s ease-in-out infinite;
  }

  @keyframes pulse {
    0%, 100% { opacity: 1; transform: scale(1); }
    50% { opacity: 0.7; transform: scale(0.95); }
  }
</style>
```

### Progress Bar Splash

```html
<style>
  .progress-container {
    width: 200px;
    height: 4px;
    background: rgba(255, 255, 255, 0.2);
    border-radius: 2px;
    overflow: hidden;
    margin-top: 20px;
  }

  .progress-bar {
    height: 100%;
    background: #4f46e5;
    animation: progress 2s ease-in-out infinite;
  }

  @keyframes progress {
    0% { width: 0%; }
    50% { width: 70%; }
    100% { width: 100%; }
  }
</style>

<div class="progress-container">
  <div class="progress-bar"></div>
</div>
```

### Dark Theme with Glow

```html
<style>
  .splash-container {
    background: #0a0a0a;
    color: #ffffff;
  }

  .logo {
    filter: drop-shadow(0 0 20px rgba(79, 70, 229, 0.5));
  }

  .app-name {
    text-shadow: 0 0 20px rgba(79, 70, 229, 0.5);
  }
</style>
```

## Important Notes

1. **Async Sleep**: Always use `tokio::time::sleep` in async Rust code, never `std::thread::sleep`. The latter blocks the entire runtime.

2. **Window Labels**: Ensure window labels in code match those in `tauri.conf.json`.

3. **Error Handling**: The splash screen should handle errors gracefully. If initialization fails, show the main window anyway with an error state.

4. **Timing**: Keep splash screen visible long enough for branding but not so long it frustrates users. Aim for 1-3 seconds minimum.

5. **Transparent Windows**: When using `transparent: true`, ensure your HTML has `background: transparent` on `html` and `body` elements.

6. **Mobile Considerations**: On mobile platforms, splash screens work differently. Consider using platform-native splash screens for iOS and Android.

## Troubleshooting

**Splash screen doesn't appear:**
- Verify the URL path is correct in `tauri.conf.json`
- Check that the HTML file exists in the correct location

**Main window shows too early:**
- Ensure `visible: false` is set on the main window
- Verify the `set_complete` command is being called correctly

**Transparent background not working:**
- Set `transparent: true` in window config
- Set `background: transparent` in CSS for html and body
- On some platforms, you may need `decorations: false`

**Window position issues:**
- Use `center: true` for centered splash screens
- Or specify explicit `x` and `y` coordinates

Overview

This skill guides you through adding a Tauri v2 splashscreen (startup/loading screen) to your application. It covers window configuration, creating custom splash HTML/CSS, wiring frontend/backend signaling to close the splash, and styling variations. The goal is a smooth startup experience that hides the main window until initialization is complete.

How this skill works

You create a small splash window (splashscreen.html) and configure the main window as hidden in the Tauri config. The frontend signals readiness via an invoke command and the Rust backend tracks tasks (frontend/backend). When all tasks are complete the backend closes the splash window and shows the main window. Optional async backend work must use tokio::time::sleep to avoid blocking the runtime.

When to use it

  • When you need a branded loading screen during app startup
  • If the app performs heavy frontend or backend initialization
  • To hide uninitialized UI while services or databases connect
  • When you want a simple progress or animated intro without blocking the main window
  • For consistent cross-platform startup presentation (desktop)

Best practices

  • Set main window visible:false in the Tauri config to prevent early display
  • Match window labels in config and code exactly (e.g., "main", "splashscreen")
  • Use asynchronous backend tasks with tokio::time::sleep instead of blocking threads
  • Keep splash duration short—aim for 1–3 seconds minimum for perceived responsiveness
  • Make transparent windows work by setting html/body background: transparent in splash HTML
  • Handle initialization errors by showing main window with an error state rather than leaving the app hidden

Example use cases

  • Branded app that needs to load config, DB, and assets before showing UI
  • App that initializes background services and should only show UI after successful connections
  • Simple apps that only need frontend readiness and can close splash via a single command
  • Apps that want different visual styles: minimal, progress bar, or glowing dark theme
  • Desktop-only projects where a custom HTML/CSS splash improves perceived speed

FAQ

How do I close the splash when both frontend and backend finish?

Have frontend and backend call the same backend command (e.g., set_complete) that toggles state; when both flags are true the backend closes the splash and shows the main window.

Why use tokio::time::sleep instead of std::thread::sleep?

std::thread::sleep blocks the async runtime and stalls other tasks. tokio::time::sleep yields properly in async code so initialization can run without freezing the app.