suiss/uim · Interaction
Island
A floating pill at the top of the screen that shows ongoing activity and short notices, and expands on press.
Island is an open-source mobile interaction from suiss/uim for React Native and Expo, with SwiftUI and Jetpack Compose versions built from the same design tokens. The @esuiss/uim CLI copies its source into your project, so you own the code.


import { BellIcon, ChatCircleIcon, CheckCircleIcon, DownloadSimpleIcon, FileZipIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Island, IslandExpandedRegion, IslandProvider, IslandRing, useIsland } from "@/components/interactions/island"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
function NotifyButtons({ children }: { children?: React.ReactNode }) {
const { notify } = useIsland()
return (
<View className="flex-row flex-wrap justify-center gap-3">
{children}
<Button
variant="secondary"
onPress={() =>
notify({
icon: (
<Icon as={ChatCircleIcon} weight="fill" className="text-green" />
),
title: "Jordan Lee",
description: "Are we still on for Saturday?",
})
}
>
<Text>New message</Text>
</Button>
<Button
variant="secondary"
onPress={() =>
notify({
icon: <Icon as={BellIcon} weight="fill" className="text-orange" />,
title: "Design review",
description: "Starts in 10 minutes",
})
}
>
<Text>Reminder</Text>
</Button>
</View>
)
}
function useDownload(initial: number | null = null) {
const [progress, setProgress] = React.useState<number | null>(initial)
const { notify } = useIsland()
React.useEffect(() => {
if (initial !== null || progress === null || progress >= 1) return
const timer = setTimeout(
() => setProgress((value) => Math.min((value ?? 0) + 0.01, 1)),
150
)
return () => clearTimeout(timer)
}, [initial, progress])
React.useEffect(() => {
if (progress !== 1) return
const timer = setTimeout(() => {
setProgress(null)
notify({
icon: (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
),
title: "Download complete",
description: "Design-Kit.zip",
})
}, 1200)
return () => clearTimeout(timer)
}, [progress, notify])
return {
progress,
start: () => setProgress(0),
cancel: () => setProgress(null),
}
}
function DownloadIsland({
progress,
onCancel,
}: {
progress: number | null
onCancel: () => void
}) {
if (progress === null) return <Island />
const done = progress >= 1
const percent = Math.round(progress * 100)
return (
<Island
accessibilityLabel={`Downloading Design-Kit.zip, ${percent}%`}
compactLeading={
<Icon as={DownloadSimpleIcon} weight="bold" className="text-accent" />
}
compactTrailing={
done ? (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
) : (
<IslandRing value={progress} />
)
}
minimal={<IslandRing value={progress} />}
>
<IslandExpandedRegion region="leading">
<View className="size-10 items-center justify-center rounded-xl bg-on-media/15">
<Icon as={FileZipIcon} weight="fill" className="size-6 text-accent" />
</View>
</IslandExpandedRegion>
<IslandExpandedRegion region="trailing">
<Text className="text-2xl font-semibold">{percent}%</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="center">
<Text numberOfLines={1} className="text-sm font-semibold">
{done ? "Downloaded" : "Downloading"}
</Text>
<Text numberOfLines={1} className="text-sm text-on-media/70">
Design-Kit.zip
</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="bottom">
<View
role="progressbar"
accessibilityLabel="Download progress"
accessibilityValue={{ min: 0, max: 100, now: percent }}
className="h-1.5 overflow-hidden rounded-full bg-on-media/20"
>
<View
className="h-full rounded-full bg-accent"
style={{ width: `${percent}%` }}
/>
</View>
<Pressable
role="button"
disabled={done}
onPress={onCancel}
className="h-11 items-center justify-center rounded-full bg-on-media/15 active:bg-on-media/25 disabled:opacity-50"
>
<Text className="text-sm font-semibold">Cancel download</Text>
</Pressable>
</IslandExpandedRegion>
</Island>
)
}
function DownloadsExample({
initial = null,
view = "compact",
withNotifications = false,
}: {
initial?: number | null
view?: "compact" | "expanded" | "minimal"
withNotifications?: boolean
}) {
const { progress, start, cancel } = useDownload(initial)
const { expand, minimize } = useIsland()
React.useEffect(() => {
if (view === "expanded") expand()
else if (view === "minimal") minimize()
}, [view, expand, minimize])
return (
<View className="flex-1 items-center justify-center gap-4">
<DownloadIsland progress={progress} onCancel={cancel} />
<Text className="max-w-xs text-center text-sm text-label-secondary">
Start a download, then tap the pill at the top to see the details. Swipe
up on it to close.
</Text>
{initial === null ? (
<Button onPress={start} disabled={progress !== null}>
<Text>
{progress === null ? "Download Design-Kit.zip" : "Downloading…"}
</Text>
</Button>
) : null}
{withNotifications ? (
<NotifyButtons>
<Button
variant="secondary"
disabled={progress === null}
onPress={minimize}
>
<Text>Minimize</Text>
</Button>
</NotifyButtons>
) : null}
</View>
)
}
export function IslandCompactExample() {
return (
<IslandProvider>
<DownloadsExample initial={0.42} />
</IslandProvider>
)
}import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.bold.DownloadSimple
import com.adamglin.phosphoricons.fill.Bell
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.CheckCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.LocalContentColor
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.Island
import com.esuiss.ui.interactions.IslandProvider
import com.esuiss.ui.interactions.IslandRegion
import com.esuiss.ui.interactions.IslandRing
import com.esuiss.ui.interactions.useIsland
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
@Composable
fun IslandCompactExample() {
IslandProvider { IslandDownloads(initial = 0.42f) }
}
enum class IslandStart { Compact, Expanded, Minimal }
@Composable
fun IslandNotifyButtons(extra: @Composable () -> Unit = {}) {
val island = useIsland()
val colors = EsuissTheme.colors
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
"New message",
onClick = {
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
},
variant = ButtonVariant.Secondary,
)
Button(
"Reminder",
onClick = {
island.notify(
title = "Design review",
description = "Starts in 10 minutes",
icon = { Icon(PhosphorIcons.Fill.Bell, contentDescription = null, tint = colors.orange) },
)
},
variant = ButtonVariant.Secondary,
)
extra()
}
}
@Composable
fun IslandProgress(progress: Float) {
val colors = EsuissTheme.colors
if (progress >= 1f) {
Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green)
} else {
IslandRing(progress)
}
}
@Composable
fun IslandDownloadIsland(progress: Float?, onCancel: () -> Unit) {
val colors = EsuissTheme.colors
val current = progress ?: 0f
val done = current >= 1f
val percent = (current * 100).roundToInt()
Island(
compactLeading = progress?.let {
{ Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, tint = colors.accent) }
},
compactTrailing = progress?.let { value -> { IslandProgress(value) } },
minimal = progress?.let { value -> { IslandProgress(value) } },
modifier = Modifier.semantics { contentDescription = "Downloading Design-Kit.zip, $percent%" },
) {
IslandExpandedRegion(IslandRegion.Leading) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.size(40.dp)
.background(foreground.copy(alpha = 0.15f), RoundedCornerShape(Radius.xl)),
contentAlignment = Alignment.Center,
) {
Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, size = 24.dp, tint = colors.accent)
}
}
IslandExpandedRegion(IslandRegion.Trailing) {
Text("$percent%", style = Typography.xl2.copy(color = LocalContentColor.current, fontWeight = FontWeight.SemiBold))
}
IslandExpandedRegion(IslandRegion.Center) {
val foreground = LocalContentColor.current
Text(
if (done) "Downloaded" else "Downloading",
style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
if (done) "Design-Kit.zip" else "Design-Kit.zip · ${(current * IslandSize).roundToInt()} of $IslandSize MB",
style = Typography.sm.copy(color = foreground.copy(alpha = 0.7f)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IslandExpandedRegion(IslandRegion.Bottom) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(CircleShape)
.background(foreground.copy(alpha = 0.2f))
.semantics {
contentDescription = "Download progress"
progressBarRangeInfo = ProgressBarRangeInfo(percent.toFloat(), 0f..100f)
},
) {
Box(
Modifier
.fillMaxWidth(current.coerceIn(0f, 1f))
.fillMaxHeight()
.background(colors.accent, CircleShape),
)
}
Spacer(Modifier.height(12.dp))
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.alpha(if (done) 0.5f else 1f)
.clip(CircleShape)
.background(foreground.copy(alpha = if (pressed) 0.25f else 0.15f))
.clickable(interactionSource = interaction, indication = null, enabled = !done, role = Role.Button, onClick = onCancel),
contentAlignment = Alignment.Center,
) {
Text("Cancel download", style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold))
}
}
}
}
@Composable
fun IslandDownloads(initial: Float? = null, start: IslandStart = IslandStart.Compact, withNotifications: Boolean = false) {
val island = useIsland()
val colors = EsuissTheme.colors
var progress by rememberSaveable { mutableStateOf(initial) }
LaunchedEffect(progress) {
val current = progress ?: return@LaunchedEffect
if (initial != null) return@LaunchedEffect
if (current < 1f) {
delay(150)
progress = minOf(current + 0.01f, 1f)
} else {
delay(1200)
progress = null
island.notify(
title = "Download complete",
description = "Design-Kit.zip",
icon = { Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green) },
)
}
}
LaunchedEffect(start) {
when (start) {
IslandStart.Expanded -> island.expand()
IslandStart.Minimal -> island.minimize()
IslandStart.Compact -> Unit
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
IslandDownloadIsland(progress) { progress = null }
Text(
"Start a download, then tap the pill at the top to see the details. Swipe up on it to close.",
modifier = Modifier.widthIn(max = 320.dp),
style = Typography.sm.copy(color = colors.labelSecondary),
textAlign = TextAlign.Center,
)
if (initial == null) {
Button(
if (progress == null) "Download Design-Kit.zip" else "Downloading…",
onClick = { progress = 0f },
enabled = progress == null,
)
}
if (withNotifications) {
IslandNotifyButtons {
Button(
"Minimize",
onClick = island::minimize,
variant = ButtonVariant.Secondary,
enabled = progress != null,
)
}
}
}
}Island floats below the top safe area and morphs between views: hidden, compact, minimal, expanded and notice. Its width, height and corner radius spring to the measured size of the active view, and views cross-fade as it changes.
Content is passed through slots. compactLeading and compactTrailing sit at the two ends of the compact pill, minimal is a single small circle, and IslandExpandedRegion children fill the expanded view. The island appears while it has compact or minimal content and hides when it has none. Pressing it expands; pressing outside, swiping up or the Android back button collapses it. notify shows a notice that replaces the current view and dismisses itself after 4 seconds, on press or on swipe up. Use it for ongoing activity such as a download, timer or call.
Installation
$ pnpm dlx @esuiss/uim@latest add island$ pnpm dlx @esuiss/uim@latest add island --platform swift$ pnpm dlx @esuiss/uim@latest add island --platform kotlinThe CLI also adds what it builds on: Icon, Text.
Manual
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/interactions/island.tsx:
import { cn } from "cn"
import type { IconProps as PhosphorIconProps } from "phosphor-react-native"
import * as React from "react"
import {
type AccessibilityActionEvent,
AccessibilityInfo,
Pressable,
useWindowDimensions,
View,
} from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
ReduceMotion,
runOnJS,
type SharedValue,
useAnimatedStyle,- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/interactions/island.tsx:
import { cn } from "cn"
import type { IconProps as PhosphorIconProps } from "phosphor-react-native"
import * as React from "react"
import {
type AccessibilityActionEvent,
AccessibilityInfo,
Pressable,
useWindowDimensions,
View,
} from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
ReduceMotion,
runOnJS,
type SharedValue,
useAnimatedStyle,- Update the import paths to match your project.
- Install the dependencies:
npx expo install phosphor-react-native react-native-gesture-handler react-native-reanimated react-native-safe-area-context react-native-svg react-native-worklets-
Add the pieces it builds on: Icon, Text, and the primitives
components/primitives/hooks.tsx,components/primitives/portal.tsx. -
Copy the source into
components/interactions/island.tsx:
import type { IconProps as PhosphorIconProps } from "phosphor-react-native"
import * as React from "react"
import {
type AccessibilityActionEvent,
AccessibilityInfo,
Pressable,
type StyleProp,
StyleSheet,
type TextStyle,
useWindowDimensions,
View,
type ViewStyle,
} from "react-native"
import { Gesture, GestureDetector } from "react-native-gesture-handler"
import {
ReduceMotion,- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Copy the theme files every view uses:
import SwiftUI
import UIKit
struct RGBA: Sendable {
let red: CGFloat
let green: CGFloat
let blue: CGFloat
let alpha: CGFloat
init(_ hex: UInt32, _ alpha: CGFloat = 1) {
red = CGFloat((hex >> 16) & 0xFF) / 255
green = CGFloat((hex >> 8) & 0xFF) / 255
blue = CGFloat(hex & 0xFF) / 255
self.alpha = alpha
}import SwiftUI
extension Color {
public static let surface = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
public static let surfaceSecondary = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
public static let surfaceTertiary = Color.adaptive(light: .init(0xE8E8ED), dark: .init(0x2C2C2E))
public static let surfaceRaised = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
public static let label = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let labelSecondary = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
public static let labelTertiary = Color.adaptive(light: .init(0x86868B), dark: .init(0x86868B))
public static let labelQuaternary = Color.adaptive(light: .init(0xD2D2D7), dark: .init(0x424245))
public static let control = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
public static let controlPressed = Color.adaptive(light: .init(0x000000, 0.12), dark: .init(0xFFFFFF, 0.22))
public static let controlSelected = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
public static let controlDisabled = Color.adaptive(light: .init(0x000000, 0.03), dark: .init(0xFFFFFF, 0.05))
public static let onControlSelected = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))- Copy the source:
import SwiftUI
enum IslandMath {
static func ringProgress(_ value: Double) -> Double {
min(max(value, 0), 1)
}
}
public struct IslandRing: View {
let value: Double
let color: Color
public init(value: Double, color: Color = .accent) {
self.value = value
self.color = color
}- Add the files to your app target.
- Set
minSdkto 24 or higher and add the dependencies:
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")- Copy the theme files every view uses:
package com.esuiss.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffsetpackage com.esuiss.ui.theme
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Immutable
data class Colors(
val surface: Color,
val surfaceSecondary: Color,
val surfaceTertiary: Color,
val surfaceRaised: Color,
val label: Color,
val labelSecondary: Color,
val labelTertiary: Color,
val labelQuaternary: Color,- Copy the source and every file it uses:
package com.esuiss.ui.interactions
import android.graphics.RectF
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsets as PlatformWindowInsets
import android.view.WindowInsetsController
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.EnterTransition
import androidx.compose.animation.ExitTransition
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.animateFloatAsStatepackage com.esuiss.ui.components
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
val LocalContentStyle = compositionLocalOf { TextStyle.Default }
val LocalContentColor = compositionLocalOf { Color.Unspecified }
val LocalIconSize = compositionLocalOf { 20.dp }package com.esuiss.ui.components
import android.provider.Settings
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.text.selection.TextSelectionColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Sizepackage com.esuiss.ui.components
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.view.View
import android.view.Window
import android.view.WindowInsetsController
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.ColumnScope- Update the
com.esuiss.uipackage names to match your project.
Usage
import { IslandProvider, IslandExpandedRegion, Island, IslandRing } from "@/components/interactions/island"<IslandProvider>
<DownloadsExample initial={0.42} />
</IslandProvider>Wrap the app in IslandProvider and render one Island. Pass compactLeading, compactTrailing and minimal only while there is activity to show, and add IslandExpandedRegions as children with a region of leading, trailing, center or bottom. Leading, center and trailing form a top row; bottom regions stack below it at full width. If only one of the compact and minimal slots is given, the island uses it for both views.
useIsland returns view, notice, expand, collapse, minimize, notify and dismiss. A notice is { icon?, title, description?, duration? }; pass a duration of 0 or Infinity to keep it until dismissed. Dragging the island pauses the notice timer. Use IslandRing for circular progress. The IslandNotice, IslandRegion and IslandView types are exported.
Examples
Expanded
Starts with the island expanded to show download details.


import { BellIcon, ChatCircleIcon, CheckCircleIcon, DownloadSimpleIcon, FileZipIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Island, IslandExpandedRegion, IslandProvider, IslandRing, useIsland } from "@/components/interactions/island"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
function NotifyButtons({ children }: { children?: React.ReactNode }) {
const { notify } = useIsland()
return (
<View className="flex-row flex-wrap justify-center gap-3">
{children}
<Button
variant="secondary"
onPress={() =>
notify({
icon: (
<Icon as={ChatCircleIcon} weight="fill" className="text-green" />
),
title: "Jordan Lee",
description: "Are we still on for Saturday?",
})
}
>
<Text>New message</Text>
</Button>
<Button
variant="secondary"
onPress={() =>
notify({
icon: <Icon as={BellIcon} weight="fill" className="text-orange" />,
title: "Design review",
description: "Starts in 10 minutes",
})
}
>
<Text>Reminder</Text>
</Button>
</View>
)
}
function useDownload(initial: number | null = null) {
const [progress, setProgress] = React.useState<number | null>(initial)
const { notify } = useIsland()
React.useEffect(() => {
if (initial !== null || progress === null || progress >= 1) return
const timer = setTimeout(
() => setProgress((value) => Math.min((value ?? 0) + 0.01, 1)),
150
)
return () => clearTimeout(timer)
}, [initial, progress])
React.useEffect(() => {
if (progress !== 1) return
const timer = setTimeout(() => {
setProgress(null)
notify({
icon: (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
),
title: "Download complete",
description: "Design-Kit.zip",
})
}, 1200)
return () => clearTimeout(timer)
}, [progress, notify])
return {
progress,
start: () => setProgress(0),
cancel: () => setProgress(null),
}
}
function DownloadIsland({
progress,
onCancel,
}: {
progress: number | null
onCancel: () => void
}) {
if (progress === null) return <Island />
const done = progress >= 1
const percent = Math.round(progress * 100)
return (
<Island
accessibilityLabel={`Downloading Design-Kit.zip, ${percent}%`}
compactLeading={
<Icon as={DownloadSimpleIcon} weight="bold" className="text-accent" />
}
compactTrailing={
done ? (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
) : (
<IslandRing value={progress} />
)
}
minimal={<IslandRing value={progress} />}
>
<IslandExpandedRegion region="leading">
<View className="size-10 items-center justify-center rounded-xl bg-on-media/15">
<Icon as={FileZipIcon} weight="fill" className="size-6 text-accent" />
</View>
</IslandExpandedRegion>
<IslandExpandedRegion region="trailing">
<Text className="text-2xl font-semibold">{percent}%</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="center">
<Text numberOfLines={1} className="text-sm font-semibold">
{done ? "Downloaded" : "Downloading"}
</Text>
<Text numberOfLines={1} className="text-sm text-on-media/70">
Design-Kit.zip
</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="bottom">
<View
role="progressbar"
accessibilityLabel="Download progress"
accessibilityValue={{ min: 0, max: 100, now: percent }}
className="h-1.5 overflow-hidden rounded-full bg-on-media/20"
>
<View
className="h-full rounded-full bg-accent"
style={{ width: `${percent}%` }}
/>
</View>
<Pressable
role="button"
disabled={done}
onPress={onCancel}
className="h-11 items-center justify-center rounded-full bg-on-media/15 active:bg-on-media/25 disabled:opacity-50"
>
<Text className="text-sm font-semibold">Cancel download</Text>
</Pressable>
</IslandExpandedRegion>
</Island>
)
}
function DownloadsExample({
initial = null,
view = "compact",
withNotifications = false,
}: {
initial?: number | null
view?: "compact" | "expanded" | "minimal"
withNotifications?: boolean
}) {
const { progress, start, cancel } = useDownload(initial)
const { expand, minimize } = useIsland()
React.useEffect(() => {
if (view === "expanded") expand()
else if (view === "minimal") minimize()
}, [view, expand, minimize])
return (
<View className="flex-1 items-center justify-center gap-4">
<DownloadIsland progress={progress} onCancel={cancel} />
<Text className="max-w-xs text-center text-sm text-label-secondary">
Start a download, then tap the pill at the top to see the details. Swipe
up on it to close.
</Text>
{initial === null ? (
<Button onPress={start} disabled={progress !== null}>
<Text>
{progress === null ? "Download Design-Kit.zip" : "Downloading…"}
</Text>
</Button>
) : null}
{withNotifications ? (
<NotifyButtons>
<Button
variant="secondary"
disabled={progress === null}
onPress={minimize}
>
<Text>Minimize</Text>
</Button>
</NotifyButtons>
) : null}
</View>
)
}
export function IslandExpandedExample() {
return (
<IslandProvider>
<DownloadsExample initial={0.42} view="expanded" />
</IslandProvider>
)
}import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.bold.DownloadSimple
import com.adamglin.phosphoricons.fill.Bell
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.CheckCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.LocalContentColor
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.Island
import com.esuiss.ui.interactions.IslandProvider
import com.esuiss.ui.interactions.IslandRegion
import com.esuiss.ui.interactions.IslandRing
import com.esuiss.ui.interactions.useIsland
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
@Composable
fun IslandExpandedExample() {
IslandProvider { IslandDownloads(initial = 0.42f, start = IslandStart.Expanded) }
}
enum class IslandStart { Compact, Expanded, Minimal }
@Composable
fun IslandNotifyButtons(extra: @Composable () -> Unit = {}) {
val island = useIsland()
val colors = EsuissTheme.colors
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
"New message",
onClick = {
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
},
variant = ButtonVariant.Secondary,
)
Button(
"Reminder",
onClick = {
island.notify(
title = "Design review",
description = "Starts in 10 minutes",
icon = { Icon(PhosphorIcons.Fill.Bell, contentDescription = null, tint = colors.orange) },
)
},
variant = ButtonVariant.Secondary,
)
extra()
}
}
@Composable
fun IslandProgress(progress: Float) {
val colors = EsuissTheme.colors
if (progress >= 1f) {
Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green)
} else {
IslandRing(progress)
}
}
@Composable
fun IslandDownloadIsland(progress: Float?, onCancel: () -> Unit) {
val colors = EsuissTheme.colors
val current = progress ?: 0f
val done = current >= 1f
val percent = (current * 100).roundToInt()
Island(
compactLeading = progress?.let {
{ Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, tint = colors.accent) }
},
compactTrailing = progress?.let { value -> { IslandProgress(value) } },
minimal = progress?.let { value -> { IslandProgress(value) } },
modifier = Modifier.semantics { contentDescription = "Downloading Design-Kit.zip, $percent%" },
) {
IslandExpandedRegion(IslandRegion.Leading) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.size(40.dp)
.background(foreground.copy(alpha = 0.15f), RoundedCornerShape(Radius.xl)),
contentAlignment = Alignment.Center,
) {
Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, size = 24.dp, tint = colors.accent)
}
}
IslandExpandedRegion(IslandRegion.Trailing) {
Text("$percent%", style = Typography.xl2.copy(color = LocalContentColor.current, fontWeight = FontWeight.SemiBold))
}
IslandExpandedRegion(IslandRegion.Center) {
val foreground = LocalContentColor.current
Text(
if (done) "Downloaded" else "Downloading",
style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
if (done) "Design-Kit.zip" else "Design-Kit.zip · ${(current * IslandSize).roundToInt()} of $IslandSize MB",
style = Typography.sm.copy(color = foreground.copy(alpha = 0.7f)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IslandExpandedRegion(IslandRegion.Bottom) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(CircleShape)
.background(foreground.copy(alpha = 0.2f))
.semantics {
contentDescription = "Download progress"
progressBarRangeInfo = ProgressBarRangeInfo(percent.toFloat(), 0f..100f)
},
) {
Box(
Modifier
.fillMaxWidth(current.coerceIn(0f, 1f))
.fillMaxHeight()
.background(colors.accent, CircleShape),
)
}
Spacer(Modifier.height(12.dp))
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.alpha(if (done) 0.5f else 1f)
.clip(CircleShape)
.background(foreground.copy(alpha = if (pressed) 0.25f else 0.15f))
.clickable(interactionSource = interaction, indication = null, enabled = !done, role = Role.Button, onClick = onCancel),
contentAlignment = Alignment.Center,
) {
Text("Cancel download", style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold))
}
}
}
}
@Composable
fun IslandDownloads(initial: Float? = null, start: IslandStart = IslandStart.Compact, withNotifications: Boolean = false) {
val island = useIsland()
val colors = EsuissTheme.colors
var progress by rememberSaveable { mutableStateOf(initial) }
LaunchedEffect(progress) {
val current = progress ?: return@LaunchedEffect
if (initial != null) return@LaunchedEffect
if (current < 1f) {
delay(150)
progress = minOf(current + 0.01f, 1f)
} else {
delay(1200)
progress = null
island.notify(
title = "Download complete",
description = "Design-Kit.zip",
icon = { Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green) },
)
}
}
LaunchedEffect(start) {
when (start) {
IslandStart.Expanded -> island.expand()
IslandStart.Minimal -> island.minimize()
IslandStart.Compact -> Unit
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
IslandDownloadIsland(progress) { progress = null }
Text(
"Start a download, then tap the pill at the top to see the details. Swipe up on it to close.",
modifier = Modifier.widthIn(max = 320.dp),
style = Typography.sm.copy(color = colors.labelSecondary),
textAlign = TextAlign.Center,
)
if (initial == null) {
Button(
if (progress == null) "Download Design-Kit.zip" else "Downloading…",
onClick = { progress = 0f },
enabled = progress == null,
)
}
if (withNotifications) {
IslandNotifyButtons {
Button(
"Minimize",
onClick = island::minimize,
variant = ButtonVariant.Secondary,
enabled = progress != null,
)
}
}
}
}Minimal
Starts in the minimal view with only the progress ring.


import { BellIcon, ChatCircleIcon, CheckCircleIcon, DownloadSimpleIcon, FileZipIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Island, IslandExpandedRegion, IslandProvider, IslandRing, useIsland } from "@/components/interactions/island"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
function NotifyButtons({ children }: { children?: React.ReactNode }) {
const { notify } = useIsland()
return (
<View className="flex-row flex-wrap justify-center gap-3">
{children}
<Button
variant="secondary"
onPress={() =>
notify({
icon: (
<Icon as={ChatCircleIcon} weight="fill" className="text-green" />
),
title: "Jordan Lee",
description: "Are we still on for Saturday?",
})
}
>
<Text>New message</Text>
</Button>
<Button
variant="secondary"
onPress={() =>
notify({
icon: <Icon as={BellIcon} weight="fill" className="text-orange" />,
title: "Design review",
description: "Starts in 10 minutes",
})
}
>
<Text>Reminder</Text>
</Button>
</View>
)
}
function useDownload(initial: number | null = null) {
const [progress, setProgress] = React.useState<number | null>(initial)
const { notify } = useIsland()
React.useEffect(() => {
if (initial !== null || progress === null || progress >= 1) return
const timer = setTimeout(
() => setProgress((value) => Math.min((value ?? 0) + 0.01, 1)),
150
)
return () => clearTimeout(timer)
}, [initial, progress])
React.useEffect(() => {
if (progress !== 1) return
const timer = setTimeout(() => {
setProgress(null)
notify({
icon: (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
),
title: "Download complete",
description: "Design-Kit.zip",
})
}, 1200)
return () => clearTimeout(timer)
}, [progress, notify])
return {
progress,
start: () => setProgress(0),
cancel: () => setProgress(null),
}
}
function DownloadIsland({
progress,
onCancel,
}: {
progress: number | null
onCancel: () => void
}) {
if (progress === null) return <Island />
const done = progress >= 1
const percent = Math.round(progress * 100)
return (
<Island
accessibilityLabel={`Downloading Design-Kit.zip, ${percent}%`}
compactLeading={
<Icon as={DownloadSimpleIcon} weight="bold" className="text-accent" />
}
compactTrailing={
done ? (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
) : (
<IslandRing value={progress} />
)
}
minimal={<IslandRing value={progress} />}
>
<IslandExpandedRegion region="leading">
<View className="size-10 items-center justify-center rounded-xl bg-on-media/15">
<Icon as={FileZipIcon} weight="fill" className="size-6 text-accent" />
</View>
</IslandExpandedRegion>
<IslandExpandedRegion region="trailing">
<Text className="text-2xl font-semibold">{percent}%</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="center">
<Text numberOfLines={1} className="text-sm font-semibold">
{done ? "Downloaded" : "Downloading"}
</Text>
<Text numberOfLines={1} className="text-sm text-on-media/70">
Design-Kit.zip
</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="bottom">
<View
role="progressbar"
accessibilityLabel="Download progress"
accessibilityValue={{ min: 0, max: 100, now: percent }}
className="h-1.5 overflow-hidden rounded-full bg-on-media/20"
>
<View
className="h-full rounded-full bg-accent"
style={{ width: `${percent}%` }}
/>
</View>
<Pressable
role="button"
disabled={done}
onPress={onCancel}
className="h-11 items-center justify-center rounded-full bg-on-media/15 active:bg-on-media/25 disabled:opacity-50"
>
<Text className="text-sm font-semibold">Cancel download</Text>
</Pressable>
</IslandExpandedRegion>
</Island>
)
}
function DownloadsExample({
initial = null,
view = "compact",
withNotifications = false,
}: {
initial?: number | null
view?: "compact" | "expanded" | "minimal"
withNotifications?: boolean
}) {
const { progress, start, cancel } = useDownload(initial)
const { expand, minimize } = useIsland()
React.useEffect(() => {
if (view === "expanded") expand()
else if (view === "minimal") minimize()
}, [view, expand, minimize])
return (
<View className="flex-1 items-center justify-center gap-4">
<DownloadIsland progress={progress} onCancel={cancel} />
<Text className="max-w-xs text-center text-sm text-label-secondary">
Start a download, then tap the pill at the top to see the details. Swipe
up on it to close.
</Text>
{initial === null ? (
<Button onPress={start} disabled={progress !== null}>
<Text>
{progress === null ? "Download Design-Kit.zip" : "Downloading…"}
</Text>
</Button>
) : null}
{withNotifications ? (
<NotifyButtons>
<Button
variant="secondary"
disabled={progress === null}
onPress={minimize}
>
<Text>Minimize</Text>
</Button>
</NotifyButtons>
) : null}
</View>
)
}
export function IslandMinimalExample() {
return (
<IslandProvider>
<DownloadsExample initial={0.42} view="minimal" />
</IslandProvider>
)
}import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.bold.DownloadSimple
import com.adamglin.phosphoricons.fill.Bell
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.CheckCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.LocalContentColor
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.Island
import com.esuiss.ui.interactions.IslandProvider
import com.esuiss.ui.interactions.IslandRegion
import com.esuiss.ui.interactions.IslandRing
import com.esuiss.ui.interactions.useIsland
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
@Composable
fun IslandMinimalExample() {
IslandProvider { IslandDownloads(initial = 0.42f, start = IslandStart.Minimal) }
}
enum class IslandStart { Compact, Expanded, Minimal }
@Composable
fun IslandNotifyButtons(extra: @Composable () -> Unit = {}) {
val island = useIsland()
val colors = EsuissTheme.colors
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
"New message",
onClick = {
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
},
variant = ButtonVariant.Secondary,
)
Button(
"Reminder",
onClick = {
island.notify(
title = "Design review",
description = "Starts in 10 minutes",
icon = { Icon(PhosphorIcons.Fill.Bell, contentDescription = null, tint = colors.orange) },
)
},
variant = ButtonVariant.Secondary,
)
extra()
}
}
@Composable
fun IslandProgress(progress: Float) {
val colors = EsuissTheme.colors
if (progress >= 1f) {
Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green)
} else {
IslandRing(progress)
}
}
@Composable
fun IslandDownloadIsland(progress: Float?, onCancel: () -> Unit) {
val colors = EsuissTheme.colors
val current = progress ?: 0f
val done = current >= 1f
val percent = (current * 100).roundToInt()
Island(
compactLeading = progress?.let {
{ Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, tint = colors.accent) }
},
compactTrailing = progress?.let { value -> { IslandProgress(value) } },
minimal = progress?.let { value -> { IslandProgress(value) } },
modifier = Modifier.semantics { contentDescription = "Downloading Design-Kit.zip, $percent%" },
) {
IslandExpandedRegion(IslandRegion.Leading) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.size(40.dp)
.background(foreground.copy(alpha = 0.15f), RoundedCornerShape(Radius.xl)),
contentAlignment = Alignment.Center,
) {
Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, size = 24.dp, tint = colors.accent)
}
}
IslandExpandedRegion(IslandRegion.Trailing) {
Text("$percent%", style = Typography.xl2.copy(color = LocalContentColor.current, fontWeight = FontWeight.SemiBold))
}
IslandExpandedRegion(IslandRegion.Center) {
val foreground = LocalContentColor.current
Text(
if (done) "Downloaded" else "Downloading",
style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
if (done) "Design-Kit.zip" else "Design-Kit.zip · ${(current * IslandSize).roundToInt()} of $IslandSize MB",
style = Typography.sm.copy(color = foreground.copy(alpha = 0.7f)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IslandExpandedRegion(IslandRegion.Bottom) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(CircleShape)
.background(foreground.copy(alpha = 0.2f))
.semantics {
contentDescription = "Download progress"
progressBarRangeInfo = ProgressBarRangeInfo(percent.toFloat(), 0f..100f)
},
) {
Box(
Modifier
.fillMaxWidth(current.coerceIn(0f, 1f))
.fillMaxHeight()
.background(colors.accent, CircleShape),
)
}
Spacer(Modifier.height(12.dp))
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.alpha(if (done) 0.5f else 1f)
.clip(CircleShape)
.background(foreground.copy(alpha = if (pressed) 0.25f else 0.15f))
.clickable(interactionSource = interaction, indication = null, enabled = !done, role = Role.Button, onClick = onCancel),
contentAlignment = Alignment.Center,
) {
Text("Cancel download", style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold))
}
}
}
}
@Composable
fun IslandDownloads(initial: Float? = null, start: IslandStart = IslandStart.Compact, withNotifications: Boolean = false) {
val island = useIsland()
val colors = EsuissTheme.colors
var progress by rememberSaveable { mutableStateOf(initial) }
LaunchedEffect(progress) {
val current = progress ?: return@LaunchedEffect
if (initial != null) return@LaunchedEffect
if (current < 1f) {
delay(150)
progress = minOf(current + 0.01f, 1f)
} else {
delay(1200)
progress = null
island.notify(
title = "Download complete",
description = "Design-Kit.zip",
icon = { Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green) },
)
}
}
LaunchedEffect(start) {
when (start) {
IslandStart.Expanded -> island.expand()
IslandStart.Minimal -> island.minimize()
IslandStart.Compact -> Unit
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
IslandDownloadIsland(progress) { progress = null }
Text(
"Start a download, then tap the pill at the top to see the details. Swipe up on it to close.",
modifier = Modifier.widthIn(max = 320.dp),
style = Typography.sm.copy(color = colors.labelSecondary),
textAlign = TextAlign.Center,
)
if (initial == null) {
Button(
if (progress == null) "Download Design-Kit.zip" else "Downloading…",
onClick = { progress = 0f },
enabled = progress == null,
)
}
if (withNotifications) {
IslandNotifyButtons {
Button(
"Minimize",
onClick = island::minimize,
variant = ButtonVariant.Secondary,
enabled = progress != null,
)
}
}
}
}Notice
Shows a notice on mount.


import { BellIcon, ChatCircleIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { Island, IslandProvider, useIsland } from "@/components/interactions/island"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
function NotifyButtons({ children }: { children?: React.ReactNode }) {
const { notify } = useIsland()
return (
<View className="flex-row flex-wrap justify-center gap-3">
{children}
<Button
variant="secondary"
onPress={() =>
notify({
icon: (
<Icon as={ChatCircleIcon} weight="fill" className="text-green" />
),
title: "Jordan Lee",
description: "Are we still on for Saturday?",
})
}
>
<Text>New message</Text>
</Button>
<Button
variant="secondary"
onPress={() =>
notify({
icon: <Icon as={BellIcon} weight="fill" className="text-orange" />,
title: "Design review",
description: "Starts in 10 minutes",
})
}
>
<Text>Reminder</Text>
</Button>
</View>
)
}
function NoticeOnMount() {
const { notify } = useIsland()
const shownRef = React.useRef(false)
React.useEffect(() => {
if (shownRef.current) return
shownRef.current = true
notify({
icon: <Icon as={ChatCircleIcon} weight="fill" className="text-green" />,
title: "Jordan Lee",
description: "Are we still on for Saturday?",
duration: 600000,
})
}, [notify])
return (
<View className="flex-1 items-center justify-center gap-4">
<Island />
<Text className="max-w-xs text-center text-sm text-label-secondary">
The pill grows to show the notification and shrinks back on its own.
</Text>
<NotifyButtons />
</View>
)
}
export function IslandNoticeExample() {
return (
<IslandProvider>
<NoticeOnMount />
</IslandProvider>
)
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.widthIn
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.fill.Bell
import com.adamglin.phosphoricons.fill.ChatCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.Island
import com.esuiss.ui.interactions.IslandProvider
import com.esuiss.ui.interactions.useIsland
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun IslandNoticeExample() {
IslandProvider { IslandNoticeOnMount() }
}
@Composable
fun IslandNotifyButtons(extra: @Composable () -> Unit = {}) {
val island = useIsland()
val colors = EsuissTheme.colors
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
"New message",
onClick = {
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
},
variant = ButtonVariant.Secondary,
)
Button(
"Reminder",
onClick = {
island.notify(
title = "Design review",
description = "Starts in 10 minutes",
icon = { Icon(PhosphorIcons.Fill.Bell, contentDescription = null, tint = colors.orange) },
)
},
variant = ButtonVariant.Secondary,
)
extra()
}
}
@Composable
fun IslandNoticeOnMount() {
val island = useIsland()
val colors = EsuissTheme.colors
var shown by rememberSaveable { mutableStateOf(false) }
LaunchedEffect(Unit) {
if (shown) return@LaunchedEffect
shown = true
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
duration = 600000L,
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Island()
Text(
"The pill grows to show the notification and shrinks back on its own.",
modifier = Modifier.widthIn(max = 320.dp),
style = Typography.sm.copy(color = colors.labelSecondary),
textAlign = TextAlign.Center,
)
IslandNotifyButtons()
}
}Live activity
A download that starts on press, with buttons that minimize the island and send notices over it.


import { BellIcon, ChatCircleIcon, CheckCircleIcon, DownloadSimpleIcon, FileZipIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { Island, IslandExpandedRegion, IslandProvider, IslandRing, useIsland } from "@/components/interactions/island"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
function NotifyButtons({ children }: { children?: React.ReactNode }) {
const { notify } = useIsland()
return (
<View className="flex-row flex-wrap justify-center gap-3">
{children}
<Button
variant="secondary"
onPress={() =>
notify({
icon: (
<Icon as={ChatCircleIcon} weight="fill" className="text-green" />
),
title: "Jordan Lee",
description: "Are we still on for Saturday?",
})
}
>
<Text>New message</Text>
</Button>
<Button
variant="secondary"
onPress={() =>
notify({
icon: <Icon as={BellIcon} weight="fill" className="text-orange" />,
title: "Design review",
description: "Starts in 10 minutes",
})
}
>
<Text>Reminder</Text>
</Button>
</View>
)
}
function useDownload(initial: number | null = null) {
const [progress, setProgress] = React.useState<number | null>(initial)
const { notify } = useIsland()
React.useEffect(() => {
if (initial !== null || progress === null || progress >= 1) return
const timer = setTimeout(
() => setProgress((value) => Math.min((value ?? 0) + 0.01, 1)),
150
)
return () => clearTimeout(timer)
}, [initial, progress])
React.useEffect(() => {
if (progress !== 1) return
const timer = setTimeout(() => {
setProgress(null)
notify({
icon: (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
),
title: "Download complete",
description: "Design-Kit.zip",
})
}, 1200)
return () => clearTimeout(timer)
}, [progress, notify])
return {
progress,
start: () => setProgress(0),
cancel: () => setProgress(null),
}
}
function DownloadIsland({
progress,
onCancel,
}: {
progress: number | null
onCancel: () => void
}) {
if (progress === null) return <Island />
const done = progress >= 1
const percent = Math.round(progress * 100)
return (
<Island
accessibilityLabel={`Downloading Design-Kit.zip, ${percent}%`}
compactLeading={
<Icon as={DownloadSimpleIcon} weight="bold" className="text-accent" />
}
compactTrailing={
done ? (
<Icon as={CheckCircleIcon} weight="fill" className="text-green" />
) : (
<IslandRing value={progress} />
)
}
minimal={<IslandRing value={progress} />}
>
<IslandExpandedRegion region="leading">
<View className="size-10 items-center justify-center rounded-xl bg-on-media/15">
<Icon as={FileZipIcon} weight="fill" className="size-6 text-accent" />
</View>
</IslandExpandedRegion>
<IslandExpandedRegion region="trailing">
<Text className="text-2xl font-semibold">{percent}%</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="center">
<Text numberOfLines={1} className="text-sm font-semibold">
{done ? "Downloaded" : "Downloading"}
</Text>
<Text numberOfLines={1} className="text-sm text-on-media/70">
Design-Kit.zip
</Text>
</IslandExpandedRegion>
<IslandExpandedRegion region="bottom">
<View
role="progressbar"
accessibilityLabel="Download progress"
accessibilityValue={{ min: 0, max: 100, now: percent }}
className="h-1.5 overflow-hidden rounded-full bg-on-media/20"
>
<View
className="h-full rounded-full bg-accent"
style={{ width: `${percent}%` }}
/>
</View>
<Pressable
role="button"
disabled={done}
onPress={onCancel}
className="h-11 items-center justify-center rounded-full bg-on-media/15 active:bg-on-media/25 disabled:opacity-50"
>
<Text className="text-sm font-semibold">Cancel download</Text>
</Pressable>
</IslandExpandedRegion>
</Island>
)
}
function DownloadsExample({
initial = null,
view = "compact",
withNotifications = false,
}: {
initial?: number | null
view?: "compact" | "expanded" | "minimal"
withNotifications?: boolean
}) {
const { progress, start, cancel } = useDownload(initial)
const { expand, minimize } = useIsland()
React.useEffect(() => {
if (view === "expanded") expand()
else if (view === "minimal") minimize()
}, [view, expand, minimize])
return (
<View className="flex-1 items-center justify-center gap-4">
<DownloadIsland progress={progress} onCancel={cancel} />
<Text className="max-w-xs text-center text-sm text-label-secondary">
Start a download, then tap the pill at the top to see the details. Swipe
up on it to close.
</Text>
{initial === null ? (
<Button onPress={start} disabled={progress !== null}>
<Text>
{progress === null ? "Download Design-Kit.zip" : "Downloading…"}
</Text>
</Button>
) : null}
{withNotifications ? (
<NotifyButtons>
<Button
variant="secondary"
disabled={progress === null}
onPress={minimize}
>
<Text>Minimize</Text>
</Button>
</NotifyButtons>
) : null}
</View>
)
}
export function IslandLiveActivityExample() {
return (
<IslandProvider>
<DownloadsExample withNotifications />
</IslandProvider>
)
}import ActivityKit
import EsuissUI
import SwiftUI
struct IslandLiveActivity: View {
@State private var activity: Activity<DownloadActivityAttributes>?
@State private var state = DownloadActivityAttributes.ContentState(progress: 0, fileName: "Design-Kit.zip", finished: false)
@State private var failure: String?
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Start a download, then leave the app. The activity shows in the Dynamic Island and on the Lock Screen.")
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
FlowRow(spacing: 12) {
Button("Start") { start() }
.buttonStyle(.variant())
.disabled(activity != nil)
Button("Update (+10%)") { advance() }
.buttonStyle(.variant(.secondary))
.disabled(activity == nil || state.finished)
Button("Finish") { finish() }
.buttonStyle(.variant(.secondary))
.disabled(activity == nil || state.finished)
Button("End") { end() }
.buttonStyle(.variant(.destructive))
.disabled(activity == nil)
}
Text(verbatim: activity == nil ? "No activity" : "\(state.fileName) · \(state.percent)%")
.textStyle(.sm, weight: .semibold)
.monospacedDigit()
if let failure {
Text(verbatim: failure)
.textStyle(.sm)
.foregroundStyle(Color.danger)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.task {
activity = Activity<DownloadActivityAttributes>.activities.first
if let current = activity?.content.state { state = current }
}
}
func start() {
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
failure = "Live Activities are turned off for this app."
return
}
let next = DownloadActivityAttributes.ContentState(progress: 0, fileName: "Design-Kit.zip", finished: false)
do {
activity = try Activity.request(
attributes: DownloadActivityAttributes(),
content: ActivityContent(state: next, staleDate: nil)
)
state = next
failure = nil
} catch {
failure = error.localizedDescription
}
}
func advance() {
state.progress = min(state.progress + 0.1, 1)
state.finished = state.progress >= 1
push()
}
func finish() {
state.progress = 1
state.finished = true
push()
}
func push() {
guard let activity else { return }
let content = ActivityContent(state: state, staleDate: nil)
Task { await activity.update(content) }
}
func end() {
guard let activity else { return }
let content = ActivityContent(state: state, staleDate: nil)
self.activity = nil
Task { await activity.end(content, dismissalPolicy: .immediate) }
}
}import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.ProgressBarRangeInfo
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.progressBarRangeInfo
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.bold.DownloadSimple
import com.adamglin.phosphoricons.fill.Bell
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.CheckCircle
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonVariant
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.LocalContentColor
import com.esuiss.ui.components.Text
import com.esuiss.ui.interactions.Island
import com.esuiss.ui.interactions.IslandProvider
import com.esuiss.ui.interactions.IslandRegion
import com.esuiss.ui.interactions.IslandRing
import com.esuiss.ui.interactions.useIsland
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
@Composable
fun IslandLiveActivityExample() {
IslandProvider { IslandDownloads(withNotifications = true) }
}
enum class IslandStart { Compact, Expanded, Minimal }
@Composable
fun IslandNotifyButtons(extra: @Composable () -> Unit = {}) {
val island = useIsland()
val colors = EsuissTheme.colors
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Button(
"New message",
onClick = {
island.notify(
title = "Jordan Lee",
description = "Are we still on for Saturday?",
icon = { Icon(PhosphorIcons.Fill.ChatCircle, contentDescription = null, tint = colors.green) },
)
},
variant = ButtonVariant.Secondary,
)
Button(
"Reminder",
onClick = {
island.notify(
title = "Design review",
description = "Starts in 10 minutes",
icon = { Icon(PhosphorIcons.Fill.Bell, contentDescription = null, tint = colors.orange) },
)
},
variant = ButtonVariant.Secondary,
)
extra()
}
}
@Composable
fun IslandProgress(progress: Float) {
val colors = EsuissTheme.colors
if (progress >= 1f) {
Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green)
} else {
IslandRing(progress)
}
}
@Composable
fun IslandDownloadIsland(progress: Float?, onCancel: () -> Unit) {
val colors = EsuissTheme.colors
val current = progress ?: 0f
val done = current >= 1f
val percent = (current * 100).roundToInt()
Island(
compactLeading = progress?.let {
{ Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, tint = colors.accent) }
},
compactTrailing = progress?.let { value -> { IslandProgress(value) } },
minimal = progress?.let { value -> { IslandProgress(value) } },
modifier = Modifier.semantics { contentDescription = "Downloading Design-Kit.zip, $percent%" },
) {
IslandExpandedRegion(IslandRegion.Leading) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.size(40.dp)
.background(foreground.copy(alpha = 0.15f), RoundedCornerShape(Radius.xl)),
contentAlignment = Alignment.Center,
) {
Icon(PhosphorIcons.Bold.DownloadSimple, contentDescription = null, size = 24.dp, tint = colors.accent)
}
}
IslandExpandedRegion(IslandRegion.Trailing) {
Text("$percent%", style = Typography.xl2.copy(color = LocalContentColor.current, fontWeight = FontWeight.SemiBold))
}
IslandExpandedRegion(IslandRegion.Center) {
val foreground = LocalContentColor.current
Text(
if (done) "Downloaded" else "Downloading",
style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
if (done) "Design-Kit.zip" else "Design-Kit.zip · ${(current * IslandSize).roundToInt()} of $IslandSize MB",
style = Typography.sm.copy(color = foreground.copy(alpha = 0.7f)),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
IslandExpandedRegion(IslandRegion.Bottom) {
val foreground = LocalContentColor.current
Box(
modifier = Modifier
.fillMaxWidth()
.height(6.dp)
.clip(CircleShape)
.background(foreground.copy(alpha = 0.2f))
.semantics {
contentDescription = "Download progress"
progressBarRangeInfo = ProgressBarRangeInfo(percent.toFloat(), 0f..100f)
},
) {
Box(
Modifier
.fillMaxWidth(current.coerceIn(0f, 1f))
.fillMaxHeight()
.background(colors.accent, CircleShape),
)
}
Spacer(Modifier.height(12.dp))
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
Box(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.alpha(if (done) 0.5f else 1f)
.clip(CircleShape)
.background(foreground.copy(alpha = if (pressed) 0.25f else 0.15f))
.clickable(interactionSource = interaction, indication = null, enabled = !done, role = Role.Button, onClick = onCancel),
contentAlignment = Alignment.Center,
) {
Text("Cancel download", style = Typography.sm.copy(color = foreground, fontWeight = FontWeight.SemiBold))
}
}
}
}
@Composable
fun IslandDownloads(initial: Float? = null, start: IslandStart = IslandStart.Compact, withNotifications: Boolean = false) {
val island = useIsland()
val colors = EsuissTheme.colors
var progress by rememberSaveable { mutableStateOf(initial) }
LaunchedEffect(progress) {
val current = progress ?: return@LaunchedEffect
if (initial != null) return@LaunchedEffect
if (current < 1f) {
delay(150)
progress = minOf(current + 0.01f, 1f)
} else {
delay(1200)
progress = null
island.notify(
title = "Download complete",
description = "Design-Kit.zip",
icon = { Icon(PhosphorIcons.Fill.CheckCircle, contentDescription = null, tint = colors.green) },
)
}
}
LaunchedEffect(start) {
when (start) {
IslandStart.Expanded -> island.expand()
IslandStart.Minimal -> island.minimize()
IslandStart.Compact -> Unit
}
}
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
IslandDownloadIsland(progress) { progress = null }
Text(
"Start a download, then tap the pill at the top to see the details. Swipe up on it to close.",
modifier = Modifier.widthIn(max = 320.dp),
style = Typography.sm.copy(color = colors.labelSecondary),
textAlign = TextAlign.Center,
)
if (initial == null) {
Button(
if (progress == null) "Download Design-Kit.zip" else "Downloading…",
onClick = { progress = 0f },
enabled = progress == null,
)
}
if (withNotifications) {
IslandNotifyButtons {
Button(
"Minimize",
onClick = island::minimize,
variant = ButtonVariant.Secondary,
enabled = progress != null,
)
}
}
}
}API Reference
IslandProvider
Holds the island mode, the notice and the notice timer.
- Type
React.ReactNode- Default
- —
IslandExpandedRegion
A region of the expanded view.
IslandExpandedRegionPropsIsland
The floating pill, rendered in a portal, with slots for the compact and minimal views.
React.ComponentProps<typeof View>- Type
string- Default
- —
- About
- Content at the start of the compact pill.
- Type
React.ReactNode- Default
- —
- About
- Content at the end of the compact pill.
- Type
React.ReactNode- Default
- —
- About
- Content of the minimal view.
- Type
React.ReactNode- Default
- —
- About
- Name of a portal host to render into.
- Type
string- Default
- —
IslandRing
A small circular progress ring.
- About
- Progress from
0to1. - Type
number- Default
- required
- Type
string- Default
- —
Accessibility
- Notices are announced with their title and description when they are text.
- The compact and minimal views are buttons that expose
expandedand haveactivateandexpandactions. - The expanded view has the
grouprole, acollapseaction and closes on the escape gesture. - The notice is a button with
activateanddismissactions. - Inactive views and the progress ring are hidden from screen readers.
Notes
- The island uses
bg-mediawithtext-on-media. - Expanded and notice views are up to 384 points wide.
- Size and fade animations respect the system reduce motion setting.