---
title: Widgets
---


## Introduction

Pre-built UI components that display Sahha data beautifully. Drop a WebView into your app, pass the profile token, and instantly show scores, factors, and biomarkers—no custom UI development required.

{% callout title="Ready to integrate?" %}
Jump to the [integration guide](#integration-guide) for your platform.
{% /callout %}

---

## Key Features

{% cards smCols=2 %}

{% card title="Drop-in Ready" description="WebView components that work in minutes—no custom UI needed" /%}
{% card title="Cross-Platform" description="Native support for iOS, Android, Flutter, React Native, and Web" /%}
{% card title="Themeable" description="Light and dark modes with automatic system preference detection" /%}
{% card title="Modular" description="Mix and match charts, arcs, bars, and factor lists" /%}
{% card title="Responsive" description="Auto-sizing components that communicate height to your app" /%}
{% card title="Customizable" description="Control dates, messages, and display options via URL params" /%}

{% /cards %}

---

## How It Works

Simply use our webview component and call one of our UI Widget endpoints with the profile token provided by our SDK. Instantly, you'll receive a dynamic and interactive chart that suits your needs. Whether you want to display a Score Chart, Score Arc, or Factor List, our widgets make it easy to present valuable insights in a visually appealing way.

---

## Available Widgets

Widgets come in two types - pages and components.

{% callout title="Light and dark theme" %}

By default, UI widgets follow the user's system preferences to determine whether to display in light or dark mode. To override this behavior, add a `theme` query parameter to the widget URL.

**Example: `?theme=light` or `?theme=dark`**

{% /callout %}

---

### Widget Pages

Widget pages are larger, comprehensive widgets that include a variety of widget components.

{% tabs %}

{% tab label="Scores" %}

{% webview-placeholder url="/images/products/widgets/scores-page.jpeg" %}

### Scores Page

The Scores Page widget visually represents an overview of all scores for the current day along with a date selector for browsing historical scores.

#### URL

https://webview.sahha.ai/app

#### URL with date

https://webview.sahha.ai/app?date=2024-05-20

#### Remove date selector

https://webview.sahha.ai/app?hideDateSelect=true

{% /webview-placeholder %}

{% /tab %}

{% tab label="Biomarkers" %}

{% webview-placeholder url="/images/products/widgets/biomarkers-page.jpeg" %}

### Biomarkers Page

The Biomarkers Page widget visually represents an overview of all biomarkers for the current day along with a date selector for browsing historical biomarkers.

#### URL

https://webview.sahha.ai/app/biomarkers

#### URL with date

https://webview.sahha.ai/app/biomarkers?date=2024-05-20

#### Remove date selector

https://webview.sahha.ai/app/biomarkers?hideDateSelect=true

{% /webview-placeholder %}

{% /tab %}

{% /tabs %}

---

### Widget Components

Widget components are lightweight, modular elements designed to display specific data in a compact format.

You can choose from a variety of styles to help you present Sahha data clearly and effectively.See below for examples and available customization options for each widget style.

---

{% webview-container route="/score/readiness/chart" %}

### Score Chart

Displays a 7-day trend of a selected score in a compact line chart, helping you visualize changes over time at a glance.

#### URL

https://webview.sahha.ai/score/`{score_type}`/chart

#### URL for specific date

https://webview.sahha.ai/score/`{score_type}`/chart?date=2024-05-20

{% /webview-container %}

---

{% webview-container route="/score/activity/arc" %}

### Score Arc

Showcases a single score using a radial progress bar, offering a simple, visual representation of progress or performance.

#### URL

https://webview.sahha.ai/score/`{score_type}`/arc

#### URL for specific date

https://webview.sahha.ai/score/`{score_type}`/arc?date=2024-05-20

#### Remove score message

https://webview.sahha.ai/score/`{score_type}`/arc?message=false

{% /webview-container %}

---

{% webview-container route="/score/wellbeing/bar" %}

### Score Bar

Presents a single score with a horizontal progress bar, ideal for quickly communicating how a score measures up within a range.

#### URL

https://webview.sahha.ai/score/`{score_type}`/bar

#### URL for specific date

https://webview.sahha.ai/score/`{score_type}`/bar?date=2024-05-20

#### Remove score message

https://webview.sahha.ai/score/`{score_type}`/arc?message=false

{% /webview-container %}

---

{% webview-container route="/score/sleep/factors" %}

### Factors List

Lists the contributing factors associated with a score, providing additional insight into what influences the overall result.

#### URL

https://webview.sahha.ai/score/`{score_type}`/factors

#### URL for specific date

https://webview.sahha.ai/score/`{score_type}`/factors?date=2024-05-20

#### Factor score with goal

https://webview.sahha.ai/score/`{score_type}`/factors?goal=true

#### Factor score as percentage

https://webview.sahha.ai/score/`{score_type}`/factors?percentage=true

#### Remove score message

https://webview.sahha.ai/score/`{score_type}`/arc?message=false

{% /webview-container %}

---

## Integration Guide

Follow this guide to easily add UI Widgets directly into your project. Choose your platform:

{% platform-cards cols=5 %}

{% platform-card label="iOS" href="#i-os" /%}
{% platform-card label="Android" href="#android" /%}
{% platform-card label="Flutter" href="#flutter" /%}
{% platform-card label="React Native" href="#react-native" /%}
{% platform-card label="Web" href="#web" /%}

{% /platform-cards %}

---

### iOS

You can show a webview in your UIKit or SwiftUI project.

#### UIKit

{% callout title="Use a WKWebView" %}

If your project uses UIKit views, you will need to create and display a **WKWebView**.

[View the WebKit - WKWebView guide.](https://developer.apple.com/documentation/webkit/wkwebview)

{% /callout %}

```swift {% title="iOS UIKit WebView integration" %}
//  WebViewController.swift

import UIKit
import WebKit
import Sahha

class WebViewController: UIViewController, WKUIDelegate {

    var webView: WKWebView!

    override func loadView() {
        let webConfiguration = WKWebViewConfiguration()
        webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.uiDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        // Use https://webview.sahha.ai/app in production

        if let url = URL(string: "https://sandbox.webview.sahha.ai/app") {
            var request = URLRequest(url: url)
            if let authToken = Sahha.profileToken {
                request.setValue(authToken, forHTTPHeaderField: "Authorization")
            }
            webView.load(request)
        }

    }

}
```

---

#### SwiftUI

{% callout title="Use a UIViewRepresentable and WKWebview" %}

If your project uses SwiftUI views, you will need to create and display a **UIViewRepresentable** and **WKWebview**.

{% /callout %}

#### Step 1) Create the UIViewRepresentable

[View the SwiftUI - UIViewRepresentable guide.](https://developer.apple.com/documentation/swiftui/uiviewrepresentable)

```swift {% title="SwiftUI UIViewRepresentable WebView" %}
//  WebView.swift

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {

    let urlString: String
    var profileToken: String?

    func makeUIView(context: Context) -> WKWebView {

        let webview = WKWebView()
        webview.isMultipleTouchEnabled = false
        return webview
    }

    func updateUIView(_ webView: WKWebView, context: Context) {

        if let url = URL(string: urlString) {
            var request = URLRequest(url: url)

            // Add authorization
            if let authToken = profileToken {
                request.setValue(authToken, forHTTPHeaderField: "Authorization")
            }

            webView.load(request)
        }
    }

}

```

#### Step 2) Display the UIViewRepresentable in a SwiftUI view

[View the WebKit - WKWebview guide.](https://developer.apple.com/documentation/webkit/wkwebview)

```swift {% title="SwiftUI view with Sahha WebView" %}
//  ContentView.swift

import SwiftUI
import Sahha

struct ContentView: View {

    var body: some View {

        // Use https://webview.sahha.ai/app in production

        WebView(urlString: "https://sandbox.webview.sahha.ai/app", profileToken: Sahha.profileToken)

    }
}

```

---

### Android

{% callout title="Use a WebView" %}

You will need to create and display a **WebView**.

[View the Webkit - WebView guide.](https://developer.android.com/reference/android/webkit/WebView)

{% /callout %}

```kotlin {% title="Android Compose WebView integration" %}
AndroidView(
    factory = {
        WebView(context).apply {
            val cookieManager = CookieManager.getInstance()
            cookieManager.setAcceptCookie(true)
            cookieManager.setAcceptThirdPartyCookies(this, true)

            layoutParams = ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT
            )

            webViewClient = WebViewClient()

            settings.apply {
                javaScriptEnabled = true
                domStorageEnabled = true
                databaseEnabled = true
                cacheMode = WebSettings.LOAD_DEFAULT
            }

            // Use https://webview.sahha.ai/app in production

            val webViewUrl = "https://sandbox.webview.sahha.ai/app"
            Sahha.profileToken?.also { token ->
                loadUrl(webViewUrl, mapOf("Authorization" to token))
            } ?: loadUrl(webViewUrl)

        }
    },
)
```

---

### Flutter

{% callout title="Use a WebView" %}

You will need to create and display a **WebView**.

[View the Flutter WebView guide.](https://pub.dev/packages/webview_flutter)

{% /callout %}

#### Step 1) Add webview_flutter as a dependency in your pubspec.yaml file

```text {% title="pubspec.yaml" highlighted-lines="5" %}
dependencies:
  flutter:
    sdk: flutter

  webview_flutter: ^4.7.0
```

#### Step 2) Instantiate a WebViewController

[View the Flutter WebViewController guide.](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewController-class.html)

#### Step 3) Pass the controller to a WebViewWidget

[View the Flutter WebViewWidget guide.](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebViewWidget-class.html)

```dart {% title="Flutter WebView with profile token" %}
import 'package:flutter/material.dart';
import 'package:sahha_flutter/sahha_flutter.dart';

import 'package:webview_flutter/webview_flutter.dart';
// Import for Android features
import 'package:webview_flutter_android/webview_flutter_android.dart';
// Import for iOS features
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';

class WebView extends StatefulWidget {
  const WebView({Key? key}) : super(key: key);

  @override
  WebState createState() => WebState();
}

class WebState extends State<WebView> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();

    // #docregion platform_features
    late final PlatformWebViewControllerCreationParams params;
    if (WebViewPlatform.instance is WebKitWebViewPlatform) {
      params = WebKitWebViewControllerCreationParams(
        allowsInlineMediaPlayback: true,
        mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
      );
    } else {
      params = const PlatformWebViewControllerCreationParams();
    }

    final WebViewController controller =
        WebViewController.fromPlatformCreationParams(params);

    controller
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..setBackgroundColor(const Color(0x00000000))
      ..setNavigationDelegate(
        NavigationDelegate(
          onProgress: (int progress) {
            debugPrint('WebView is loading (progress : $progress%)');
          },
          onPageStarted: (String url) {
            debugPrint('Page started loading: $url');
          },
          onPageFinished: (String url) {
            debugPrint('Page finished loading: $url');
          },
          onWebResourceError: (WebResourceError error) {
            debugPrint('''
Page resource error:
  code: ${error.errorCode}
  description: ${error.description}
  errorType: ${error.errorType}
  isForMainFrame: ${error.isForMainFrame}
          ''');
          },
          onNavigationRequest: (NavigationRequest request) {
            if (request.url.startsWith('https://www.youtube.com/')) {
              debugPrint('blocking navigation to ${request.url}');
              return NavigationDecision.prevent;
            }
            debugPrint('allowing navigation to ${request.url}');
            return NavigationDecision.navigate;
          },
          onHttpError: (HttpResponseError error) {
            debugPrint('Error occurred on page: ${error.response?.statusCode}');
          },
          onUrlChange: (UrlChange change) {
            debugPrint('url change to ${change.url}');
          },
          onHttpAuthRequest: (HttpAuthRequest request) {
            debugPrint('auth request $request');
          },
        ),
      )

    // #docregion platform_features
    if (controller.platform is AndroidWebViewController) {
      AndroidWebViewController.enableDebugging(true);
      (controller.platform as AndroidWebViewController)
          .setMediaPlaybackRequiresUserGesture(false);
    }
    // #enddocregion platform_features

    _controller = controller;

    SahhaFlutter.getProfileToken().then((value) {
      debugPrint(value);
      if (value != null) {
        controller.loadRequest(

            // Use https://webview.sahha.ai/app in production

            Uri.parse("https://sandbox.webview.sahha.ai/app"),
            headers: {"Authorization": value},
        );
      } else {
        controller.loadRequest(

            // Use https://webview.sahha.ai/app in production

            Uri.parse("https://sandbox.webview.sahha.ai/app"),
        );
      }
    }).catchError((error, stackTrace) {
      debugPrint(error.toString());
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Webview Example')),
      body: WebViewWidget(controller: _controller),
    );
  }

}
```

---

### React Native

{% callout title="Use a WebView" %}

You will need to create and display a **WebView**.

[View the React Native WebView guide.](https://github.com/react-native-webview/react-native-webview)

{% /callout %}

#### Step 1) Install react-native-webview via npm

```text {% title="Terminal" %}
npm install react-native-webview
```

#### Step 2) Instantiate a WebViewController

```typescript {% title="React Native WebView with auth header" %}
import React, { Component } from 'react';
import { WebView } from 'react-native-webview';
import Sahha from 'sahha-react-native';

const MyWebComponent = () => {

    const [profileToken, setProfileToken] = useState<string>('');

    useEffect(() => {
        Sahha.getProfileToken((error: string, token?: string) => {
            if (error) {
                console.error(`Error: ${error}`);
            } else if (token) {
                console.log(`Profile Token: ${token}`);
                setProfileToken(token);
            } else {
                console.log(`Profile Token: null`);
            }
        });
    }, []);

    // Ensure profile token has a value before rendering the webview

    if(!profileToken) return null;

    // Use https://webview.sahha.ai/app in production

    return <WebView source={{
        uri: "https://sandbox.webview.sahha.ai/app",
        headers: {
            'Authorization': profileToken,
        },
    }} style={{ flex: 1 }} />;
}
```

---

### Web

{% callout title="Use an HTML iFrame" %}

You will need to create and display an **HTML iFrame**.

[View the HTML iFrame guide.](https://www.w3schools.com/html/html_iframe.asp)

{% /callout %}

{% callout title="Route prefix required" type="warning" %}

When embedding webviews inside of an iFrame, **all route names must be prefixed with `/web`**.

For example, to use the sleep chart widget, the URL should be: `https://webview.sahha.ai/web/score/sleep/chart`

{% /callout %}

```html {% title="Embed Sahha widget via iFrame" %}
<!doctype html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Sahha UI Widget Example</title>
		<script>
			// You will need to authenticate your user profile via the Sahha SDK or API
			// Once you receive a profile token, set it here
			const profileToken = 'PROFILE_TOKEN'; // Replace 'PROFILE_TOKEN' with the real profile token

			// Origin for the webview
			// Use 'https://webview.sahha.ai' for your production app
			const origin = 'https://sandbox.webview.sahha.ai';

			// The source URL for the iFrame
			// This should be the full URL of the Sahha widget you want to render inside the iFrame.
			// The URL must use the same `origin` as defined above to ensure proper communication between the parent page and the iFrame.
			const source = origin + '/web/app'; // Example widget path: "/web/app"

			// Event listener for message event
			window.addEventListener('message', (event) => {
				if (event.origin !== origin) return;

				if (event.data.loaded) {
					event.source?.postMessage(profileToken, origin);
				}
			});

			// Create and inject the iFrame into your HTML page
			document.addEventListener('DOMContentLoaded', () => {
				const iframe = document.createElement('iframe');

				iframe.src = source;
				iframe.title = 'Sahha Webview';
				iframe.style.width = '100%';
				iframe.style.height = '1000px';
				iframe.style.border = 'none';

				document.body.appendChild(iframe);
			});
		</script>
	</head>
	<body></body>
</html>
```

---

## Retrieving Widget Height

### Overview

The Sahha WebView automatically sends its current height to the native application whenever its size changes. This allows dynamic resizing of the container holding the WebView to match its content.

Communication happens via:

- Calling `NativeBridge.postMessage` (if injected into the page)
- Fallback to `window.parent.postMessage` if running inside an iframe

Additionally, each WebView can accept a unique id query parameter, so your app knows which WebView the message originated from.

---

### Requirements

To enable communication:

- **Inject a NativeBridge** into the WebView's JavaScript context.
- **Handle `postMessage`** in your platform's native WebView API.
- **Pass an `id`** as a query parameter on the WebView's URL.

Example URL:

```text {% title="WebView URL with custom ID parameter" %}
https://webview.sahha.ai/score/activity/arc?id=my-custom-webview-id
```

---

### Injecting NativeBridge

The WebView must have this small JavaScript snippet injected before page scripts run:

```typescript {% title="Inject NativeBridge for height messages" %}
(function () {
	if (typeof window.NativeBridge === 'undefined') {
		window.NativeBridge = {
			postMessage: function (message) {
				// Forward message to your platform's onMessage or equivalent handler
				window.ReactNativeWebView?.postMessage?.(message); // Example for React Native using react-native-webview
			}
		};
	}
})();
```

**Important:**

- **You must adapt `window.ReactNativeWebView.postMessage`** to match your platform's WebView message handling method.
- If you are using Flutter, Android, iOS, or others, replace the postMessage line appropriately.

---

### Message Format

The Sahha WebView sends messages to the host application when its height changes. The structure is always:

```json {% title="WebView height change message" %}
{
	"type": "webview:height",
	"height": 1234, // Content height in pixels
	"id": "your-webview-id" // From URL query param
}
```

But the message type depends on the transport:

| Context                              | Sent As              | Parse Required?      |
| ------------------------------------ | -------------------- | -------------------- |
| `NativeBridge`                       | **Stringified JSON** | ✅ Yes               |
| `window.parent.postMessage` (iframe) | **Raw object**       | ❌ No parsing needed |

#### Why This Matters

Your platform must detect which environment it's in:

- If you **inject a** `NativeBridge`, your handler receives a JSON string, and **you must parse it**.
- If you're embedding Sahha in an **iframe**, the `message` will arrive in a `message` event as a **JavaScript object**, and is ready to use.

---

### Troubleshooting

| Problem                                 | Cause                                        | Solution                                     |
| --------------------------------------- | -------------------------------------------- | -------------------------------------------- |
| No messages received                    | `NativeBridge` not injected                  | Ensure injection code runs early             |
| Message received but no `id`            | Missing `id` in WebView URL                  | Add `?id=your-id` to WebView URL             |
| Wrong platform code in injection        | `postMessage` incorrectly mapped             | Match `NativeBridge.postMessage` to platform |
| Messages received but can't access data | Message is a JSON string and not a JS object | Ensure you are parsing messages properly     |

---

## Custom UI

Want to build your own UI instead? Use Sahha's API to fetch scores and biomarkers, then render them however you like.

**Tutorials:**

- [Build a webapp with Sahha in 30 minutes](https://resources.sahha.ai/build/webapp-30-minutes)
- [Visualizing Sahha Scores in React-Native](https://resources.sahha.ai/build/visualizing-scores)

---

## FAQ

{% faq %}

{% faq-item question="Do widgets work offline?" %}
No. Widgets require an internet connection to fetch data from Sahha's servers.
{% /faq-item %}

{% faq-item question="Can I customize the colors?" %}
Widgets support light and dark themes. Custom brand colors are on the roadmap.
{% /faq-item %}

{% faq-item question="What's the difference between sandbox and production URLs?" %}
Use `sandbox.webview.sahha.ai` for testing and `webview.sahha.ai` for production.
{% /faq-item %}

{% /faq %}

---

## Support

For help with Widgets, reach out in the [Slack community](https://join.slack.com/t/sahhacommunity/shared_invite/zt-1w0fmfbvk-qUwQ83tJgXyjT9XSxJvKIw) or contact [support@sahha.ai](mailto:support@sahha.ai).
