
Metaball-style "gooey" UI toolkit creating merging blobs and liquid bridges for transfers, notched cards, and sliding indicators using smooth SDF blending with pluggable GPU or CPU renderers.
🫧 Metaball-style "gooey" UI for Compose Multiplatform: blobs that merge, stretch a neck, and pinch apart as items animate from a source into a target.
Reactify renders a frame as a GooeyScene: a list of blobs (round rects, circles, capsules) that
smoothly merge via a polynomial smooth-min, plus cutouts that smoothly subtract. On top of that
SDF core it builds three ready-to-use pieces:
Rendering is pluggable: AGSL (RuntimeShader) on Android API 33+, SkSL via Skiko on iOS, or a
CPU path-tracing fallback everywhere else, pick automatically with GooeyRenderers.auto().
| Transfer | Bottom Nav Indicator | Segmented Control | |
|---|---|---|---|
| Android | ![]() |
![]() |
![]() |
| iOS | ![]() |
![]() |
![]() |
// Compose Multiplatform (Android + iOS)
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.veroanggraa:reactify:0.1.0-alpha01")
}
}
}// Android only
dependencies {
implementation("io.github.veroanggraa:reactify:0.1.0-alpha01")
}Supported targets: android, iosArm64, iosSimulatorArm64, iosX64.
Wrap the screen in a GooeyTransferHost, mark each source row with GooeySource, and give the
target card a GooeyTarget. Tapping (or long-press-dragging) a source transfers its token.
val state = rememberGooeyTransferState()
GooeyTransferHost(state = state) {
Column {
GooeyTarget(state = state, cornerRadius = 28.dp) {
TransferredTokens()
}
LazyColumn {
items(friends, key = { it.id }) { friend ->
GooeySource(
state = state,
key = friend.id,
color = friend.tint,
cornerRadius = 16.dp,
token = { Avatar(friend) },
) {
FriendRow(friend, avatarModifier = Modifier.tokenAnchor())
}
}
}
}
}tokenAnchor() marks where inside the row the flying copy visually starts from. Pair the target
with a GooeyNotchShape to make the card's background open a matching notch as tokens arrive:
val notchRadius by animateDpAsState(if (collapsed) 30.dp else 0.dp)
GooeyTarget(
state = state,
cornerRadius = 28.dp,
notchRadius = notchRadius,
modifier = Modifier.background(CardYellow, GooeyNotchShape(cornerRadius = 28.dp, notchRadius = notchRadius)),
) {
TransferredTokens()
}val state = rememberGooeyNavIndicatorState(selectedIndex = 0)
GooeyNavIndicatorHost(state = state, color = Mint) {
NavigationBar {
tabs.forEachIndexed { index, tab ->
NavigationBarItem(
modifier = Modifier.gooeyNavItem(state, index),
selected = state.selectedIndex == index,
onClick = { state.select(index) },
icon = { /* ... */ },
)
}
}
}val state = rememberGooeySegmentedControlState(selectedIndex = 0)
GooeySegmentedControlHost(state = state, color = Mint) {
Row {
options.forEachIndexed { index, label ->
Box(
Modifier
.gooeySegmentItem(state, index)
.clickable { state.select(index) },
) { Text(label) }
}
}
}GooeyRenderers.auto() // AGSL on Android 33+, SkSL on iOS, CPU path tracing elsewhere
GooeyRenderers.path() // force the CPU fallback
GooeyRenderers.plain() // draw every blob independently, no merging — useful for debugging layoutsPass the renderer explicitly to any host (GooeyTransferHost, GooeyNavIndicatorHost,
GooeySegmentedControlHost) if you don't want the default.
The app module (Android) and iosApp (iOS, via samples-shared) both run the same demo: an
invite-friends list, a bottom-nav indicator, and a segmented control, switchable from one screen.
Open iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme. The simulator works out of
the box.
To run on a physical device, create iosApp/Configuration/Config.local.xcconfig (git-ignored) with
your own signing values:
TEAM_ID = ABCDE12345
BUNDLE_ID = com.yourname.reactify.iosapp
Your Team ID is listed in Xcode → Settings → Accounts. A free Personal Team works; the bundle ID just has to be unique to you. Prefer this file over picking a team in Signing & Capabilities, which writes your team into the shared project file.
Copyright 2026 Veronica
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See LICENSE for the full text.
🫧 Metaball-style "gooey" UI for Compose Multiplatform: blobs that merge, stretch a neck, and pinch apart as items animate from a source into a target.
Reactify renders a frame as a GooeyScene: a list of blobs (round rects, circles, capsules) that
smoothly merge via a polynomial smooth-min, plus cutouts that smoothly subtract. On top of that
SDF core it builds three ready-to-use pieces:
Rendering is pluggable: AGSL (RuntimeShader) on Android API 33+, SkSL via Skiko on iOS, or a
CPU path-tracing fallback everywhere else, pick automatically with GooeyRenderers.auto().
| Transfer | Bottom Nav Indicator | Segmented Control | |
|---|---|---|---|
| Android | ![]() |
![]() |
![]() |
| iOS | ![]() |
![]() |
![]() |
// Compose Multiplatform (Android + iOS)
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.veroanggraa:reactify:0.1.0-alpha01")
}
}
}// Android only
dependencies {
implementation("io.github.veroanggraa:reactify:0.1.0-alpha01")
}Supported targets: android, iosArm64, iosSimulatorArm64, iosX64.
Wrap the screen in a GooeyTransferHost, mark each source row with GooeySource, and give the
target card a GooeyTarget. Tapping (or long-press-dragging) a source transfers its token.
val state = rememberGooeyTransferState()
GooeyTransferHost(state = state) {
Column {
GooeyTarget(state = state, cornerRadius = 28.dp) {
TransferredTokens()
}
LazyColumn {
items(friends, key = { it.id }) { friend ->
GooeySource(
state = state,
key = friend.id,
color = friend.tint,
cornerRadius = 16.dp,
token = { Avatar(friend) },
) {
FriendRow(friend, avatarModifier = Modifier.tokenAnchor())
}
}
}
}
}tokenAnchor() marks where inside the row the flying copy visually starts from. Pair the target
with a GooeyNotchShape to make the card's background open a matching notch as tokens arrive:
val notchRadius by animateDpAsState(if (collapsed) 30.dp else 0.dp)
GooeyTarget(
state = state,
cornerRadius = 28.dp,
notchRadius = notchRadius,
modifier = Modifier.background(CardYellow, GooeyNotchShape(cornerRadius = 28.dp, notchRadius = notchRadius)),
) {
TransferredTokens()
}val state = rememberGooeyNavIndicatorState(selectedIndex = 0)
GooeyNavIndicatorHost(state = state, color = Mint) {
NavigationBar {
tabs.forEachIndexed { index, tab ->
NavigationBarItem(
modifier = Modifier.gooeyNavItem(state, index),
selected = state.selectedIndex == index,
onClick = { state.select(index) },
icon = { /* ... */ },
)
}
}
}val state = rememberGooeySegmentedControlState(selectedIndex = 0)
GooeySegmentedControlHost(state = state, color = Mint) {
Row {
options.forEachIndexed { index, label ->
Box(
Modifier
.gooeySegmentItem(state, index)
.clickable { state.select(index) },
) { Text(label) }
}
}
}GooeyRenderers.auto() // AGSL on Android 33+, SkSL on iOS, CPU path tracing elsewhere
GooeyRenderers.path() // force the CPU fallback
GooeyRenderers.plain() // draw every blob independently, no merging — useful for debugging layoutsPass the renderer explicitly to any host (GooeyTransferHost, GooeyNavIndicatorHost,
GooeySegmentedControlHost) if you don't want the default.
The app module (Android) and iosApp (iOS, via samples-shared) both run the same demo: an
invite-friends list, a bottom-nav indicator, and a segmented control, switchable from one screen.
Open iosApp/iosApp.xcodeproj in Xcode and run the iosApp scheme. The simulator works out of
the box.
To run on a physical device, create iosApp/Configuration/Config.local.xcconfig (git-ignored) with
your own signing values:
TEAM_ID = ABCDE12345
BUNDLE_ID = com.yourname.reactify.iosapp
Your Team ID is listed in Xcode → Settings → Accounts. A free Personal Team works; the bundle ID just has to be unique to you. Prefer this file over picking a team in Signing & Capabilities, which writes your team into the shared project file.
Copyright 2026 Veronica
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
See LICENSE for the full text.