
Language-learning flashcard UI handling full card lifecycle: 3D flips, settled-lock, confirm/dismiss exit animations, audio button states, language pills, ghost stack, and extensive styling/customization.
A language-learning flashcard component for Compose Multiplatform (Android & iOS).
CuteCard handles the full interaction lifecycle of a single flashcard - 3D flip animation, settled-lock delay, confirm/dismiss exit animations, and audio button visuals - so you can focus on your deck logic and data.
implementation("site.llinsoft:cutecard:0.2.10")For the full API reference see documentation/CuteCard_Documentation.md.
CuteCard(
content = CuteCardContent(
word = "iron",
translation = "hierro",
phonetics = "[ˈje.ro]",
wordClass = "noun",
audioUrl = "https://example.com/hierro.mp3"
),
onKnown = { /* advance to next card */ },
onUnknown = { /* re-queue or move on */ },
onAudioRequested = { player.play(content.audioUrl) }
)onKnown and onUnknown are called after their respective exit animations complete — replace the card there.
CuteCardContent holds all data for a single card. One instance = one card.
CuteCardContent(
word = "correr", // required — the word to learn
translation = "to run", // required — the translation
phonetics = "[ko.ˈrer]", // optional — hides phonetics row when null
wordClass = "verb", // optional — hides word class pill when null
audioUrl = "https://...", // optional — hides audio button when null
sourceLanguage = "ES", // optional — language code shown on the word face (null = hidden)
targetLanguage = "EN" // optional — language code shown on the translation face (null = hidden)
)Changing content while the component is in composition resets the card to its front face.
Control which face appears first via CuteCardConfig.frontSide.
| Mode | Front face | Back face |
|---|---|---|
CardFrontSide.Word (default)
|
Word only | Translation + phonetics + audio |
CardFrontSide.Translation |
Translation + phonetics + audio | Word only |
// Mode B — see the full info, recall the word
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
config = CuteCardConfig(frontSide = CardFrontSide.Translation)
)CuteCardConfig controls all behavior and animation. All fields have sensible defaults.
CuteCardConfig(
frontSide = CardFrontSide.Word, // which face shows first
flipDurationMs = 400, // 3D flip duration
settledLockDurationMs = 350, // tap-lock after flip (prevents accidental confirm)
exitDurationMs = 300, // exit animation duration
flipDirection = FlipDirection.Horizontal, // or Vertical
confirmExit = ExitAnimation.SlideUp, // animation when marked as known
dismissExit = ExitAnimation.SlideDown, // animation when marked as unknown
)| Value | Effect |
|---|---|
SlideUp |
Card slides up and fades out |
SlideDown |
Card slides down and fades out |
ScaleFade |
Card scales up slightly then fades out |
Fade |
Card fades out in place |
None |
Card disappears instantly (no animation) |
confirmExit and dismissExit are independent. Use None to disable animation.
Build from CuteCardDefaults.style() and override only what you need.
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
style = CuteCardDefaults.style().copy(
cardBackgroundColor = Color(0xFF1E1E1E),
wordTextColor = Color(0xFFF0EFE9)
)
)A pre-built dark style ships out of the box:
style = CuteCardDefaults.darkStyle()CuteCardStyle exposes:
| Field | Controls |
|---|---|
cardShape |
Card corner shape |
cardElevation |
Shadow depth of the active card |
cardAspectRatio |
Width-to-height ratio (default 3:4) |
cardBackgroundColor |
Card surface fill |
ghostCardBackgroundColor |
Stack cards behind the active card |
cardBorderColor |
Optional card stroke (transparent = none) |
wordTextStyle / wordTextColor
|
Primary word / translation typography |
phoneticsTextStyle / phoneticsTextColor
|
Phonetics row typography |
wordClassPillStyle |
Word class chip — text, colors, shape |
audioButtonStyle |
Audio button — icon colors, stroke widths, shape, typography |
dismissButtonStyle |
Dismiss button — text color, typography, shape |
languagePillStyle |
Language indicator pill — text style, colors, shape, padding, corner offset. backTextColor / backContainerColor override colors on the back face independently |
The front and back faces can have different pill colors. Set backTextColor and/or backContainerColor on languagePillStyle — they fall back to the base colors when not set.
style = CuteCardDefaults.style().copy(
languagePillStyle = CuteCardDefaults.style().languagePillStyle.copy(
containerColor = Color(0xFFDCE3EE), // front face pill background
textColor = Color(0xFF1F3A5F), // front face pill text
backContainerColor = Color(0xFF2E4A3E), // back face pill background
backTextColor = Color(0xFFAADDC4) // back face pill text
)
)CuteCardLabels collects all user-facing strings in one place. Pass it to override the language or wording — defaults are English.
Visible text
dismissButtonLabel — text on the default "I don't know" button.audioButtonIdleLabel / audioButtonPlayingLabel — text shown on the audio button in its idle and playing states.Accessibility labels (screen readers only, never shown visually)
unflipButtonLabel — content description for the unflip icon button.audioButtonContentDescription, cardFrontContentDescription, cardBackContentDescription.CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
labels = CuteCardLabels(
dismissButtonLabel = "Noch nicht",
unflipButtonLabel = "Wort anzeigen",
audioButtonIdleLabel = "Anhören",
audioButtonPlayingLabel = "Läuft...",
audioButtonContentDescription = "Aussprache abspielen",
cardFrontContentDescription = "Wortkarte, tippen zum Aufdecken",
cardBackContentDescription = "Übersetzung sichtbar, tippen zum Bestätigen"
)
)The audio button appears on the full-info face when onAudioRequested is non-null. The library manages visuals only — playback is entirely the consumer's responsibility.
var isPlaying by remember { mutableStateOf(false) }
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
isPlaying = isPlaying,
onAudioRequested = {
isPlaying = true
player.play(content.audioUrl) {
isPlaying = false // called when playback ends
}
}
)When isPlaying is true the button switches to an accent color and thicker stroke. When onAudioRequested is null, the button is hidden entirely with no empty space left behind.
Two optional callbacks let you react to flip events without polling state.
onFlipped — fires when the back face becomes interactive (flip animation done + settle lock expired). Use it to auto-play audio or log analytics.
onFlippedBack — fires when the user taps the back face to confirm the card as known, before the exit animation starts. Useful for immediate UI reactions.
CuteCard(
content = content,
onKnown = { /* advance deck */ },
onUnknown = { /* re-queue */ },
onFlipped = { player.play(content.audioUrl) }, // auto-play on reveal
onFlippedBack = { viewModel.onConfirmTapped() } // react before exit
)Both default to null (no callback).
The dismiss button slot accepts any composable. The onClick lambda passed to it must be called to trigger the dismiss animation.
// Icon button
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
dismissButton = { onClick ->
IconButton(onClick = onClick) {
Icon(Icons.Default.Close, contentDescription = "I don't know")
}
}
)
// Image button
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
dismissButton = { onClick ->
Image(
painter = painterResource(Res.drawable.thumbs_down),
contentDescription = "I don't know",
modifier = Modifier.clickable(onClick = onClick)
)
}
)The unflip button is disabled by default (unflipButton = null). When enabled, it appears above the card (top-right) while the back face is visible. Tapping it plays the flip animation in reverse, returning to the front face without dismissing the card.
Enable it by supplying any composable — the onClick lambda must be called to trigger the animation:
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
unflipButton = { onClick ->
IconButton(onClick = onClick) {
Icon(Icons.Default.Replay, contentDescription = "Show word")
}
}
)When null, no space is reserved above the card.
MIT — see LICENSE for details.
# Build all targets
./gradlew :cutecard:build
# Run common tests
./gradlew :cutecard:allTests
# Build Android artifact
./gradlew :cutecard:assemble
# Build iOS frameworks
./gradlew :cutecard:linkDebugFrameworkIosArm64 \
:cutecard:linkDebugFrameworkIosSimulatorArm64A language-learning flashcard component for Compose Multiplatform (Android & iOS).
CuteCard handles the full interaction lifecycle of a single flashcard - 3D flip animation, settled-lock delay, confirm/dismiss exit animations, and audio button visuals - so you can focus on your deck logic and data.
implementation("site.llinsoft:cutecard:0.2.10")For the full API reference see documentation/CuteCard_Documentation.md.
CuteCard(
content = CuteCardContent(
word = "iron",
translation = "hierro",
phonetics = "[ˈje.ro]",
wordClass = "noun",
audioUrl = "https://example.com/hierro.mp3"
),
onKnown = { /* advance to next card */ },
onUnknown = { /* re-queue or move on */ },
onAudioRequested = { player.play(content.audioUrl) }
)onKnown and onUnknown are called after their respective exit animations complete — replace the card there.
CuteCardContent holds all data for a single card. One instance = one card.
CuteCardContent(
word = "correr", // required — the word to learn
translation = "to run", // required — the translation
phonetics = "[ko.ˈrer]", // optional — hides phonetics row when null
wordClass = "verb", // optional — hides word class pill when null
audioUrl = "https://...", // optional — hides audio button when null
sourceLanguage = "ES", // optional — language code shown on the word face (null = hidden)
targetLanguage = "EN" // optional — language code shown on the translation face (null = hidden)
)Changing content while the component is in composition resets the card to its front face.
Control which face appears first via CuteCardConfig.frontSide.
| Mode | Front face | Back face |
|---|---|---|
CardFrontSide.Word (default)
|
Word only | Translation + phonetics + audio |
CardFrontSide.Translation |
Translation + phonetics + audio | Word only |
// Mode B — see the full info, recall the word
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
config = CuteCardConfig(frontSide = CardFrontSide.Translation)
)CuteCardConfig controls all behavior and animation. All fields have sensible defaults.
CuteCardConfig(
frontSide = CardFrontSide.Word, // which face shows first
flipDurationMs = 400, // 3D flip duration
settledLockDurationMs = 350, // tap-lock after flip (prevents accidental confirm)
exitDurationMs = 300, // exit animation duration
flipDirection = FlipDirection.Horizontal, // or Vertical
confirmExit = ExitAnimation.SlideUp, // animation when marked as known
dismissExit = ExitAnimation.SlideDown, // animation when marked as unknown
)| Value | Effect |
|---|---|
SlideUp |
Card slides up and fades out |
SlideDown |
Card slides down and fades out |
ScaleFade |
Card scales up slightly then fades out |
Fade |
Card fades out in place |
None |
Card disappears instantly (no animation) |
confirmExit and dismissExit are independent. Use None to disable animation.
Build from CuteCardDefaults.style() and override only what you need.
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
style = CuteCardDefaults.style().copy(
cardBackgroundColor = Color(0xFF1E1E1E),
wordTextColor = Color(0xFFF0EFE9)
)
)A pre-built dark style ships out of the box:
style = CuteCardDefaults.darkStyle()CuteCardStyle exposes:
| Field | Controls |
|---|---|
cardShape |
Card corner shape |
cardElevation |
Shadow depth of the active card |
cardAspectRatio |
Width-to-height ratio (default 3:4) |
cardBackgroundColor |
Card surface fill |
ghostCardBackgroundColor |
Stack cards behind the active card |
cardBorderColor |
Optional card stroke (transparent = none) |
wordTextStyle / wordTextColor
|
Primary word / translation typography |
phoneticsTextStyle / phoneticsTextColor
|
Phonetics row typography |
wordClassPillStyle |
Word class chip — text, colors, shape |
audioButtonStyle |
Audio button — icon colors, stroke widths, shape, typography |
dismissButtonStyle |
Dismiss button — text color, typography, shape |
languagePillStyle |
Language indicator pill — text style, colors, shape, padding, corner offset. backTextColor / backContainerColor override colors on the back face independently |
The front and back faces can have different pill colors. Set backTextColor and/or backContainerColor on languagePillStyle — they fall back to the base colors when not set.
style = CuteCardDefaults.style().copy(
languagePillStyle = CuteCardDefaults.style().languagePillStyle.copy(
containerColor = Color(0xFFDCE3EE), // front face pill background
textColor = Color(0xFF1F3A5F), // front face pill text
backContainerColor = Color(0xFF2E4A3E), // back face pill background
backTextColor = Color(0xFFAADDC4) // back face pill text
)
)CuteCardLabels collects all user-facing strings in one place. Pass it to override the language or wording — defaults are English.
Visible text
dismissButtonLabel — text on the default "I don't know" button.audioButtonIdleLabel / audioButtonPlayingLabel — text shown on the audio button in its idle and playing states.Accessibility labels (screen readers only, never shown visually)
unflipButtonLabel — content description for the unflip icon button.audioButtonContentDescription, cardFrontContentDescription, cardBackContentDescription.CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
labels = CuteCardLabels(
dismissButtonLabel = "Noch nicht",
unflipButtonLabel = "Wort anzeigen",
audioButtonIdleLabel = "Anhören",
audioButtonPlayingLabel = "Läuft...",
audioButtonContentDescription = "Aussprache abspielen",
cardFrontContentDescription = "Wortkarte, tippen zum Aufdecken",
cardBackContentDescription = "Übersetzung sichtbar, tippen zum Bestätigen"
)
)The audio button appears on the full-info face when onAudioRequested is non-null. The library manages visuals only — playback is entirely the consumer's responsibility.
var isPlaying by remember { mutableStateOf(false) }
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
isPlaying = isPlaying,
onAudioRequested = {
isPlaying = true
player.play(content.audioUrl) {
isPlaying = false // called when playback ends
}
}
)When isPlaying is true the button switches to an accent color and thicker stroke. When onAudioRequested is null, the button is hidden entirely with no empty space left behind.
Two optional callbacks let you react to flip events without polling state.
onFlipped — fires when the back face becomes interactive (flip animation done + settle lock expired). Use it to auto-play audio or log analytics.
onFlippedBack — fires when the user taps the back face to confirm the card as known, before the exit animation starts. Useful for immediate UI reactions.
CuteCard(
content = content,
onKnown = { /* advance deck */ },
onUnknown = { /* re-queue */ },
onFlipped = { player.play(content.audioUrl) }, // auto-play on reveal
onFlippedBack = { viewModel.onConfirmTapped() } // react before exit
)Both default to null (no callback).
The dismiss button slot accepts any composable. The onClick lambda passed to it must be called to trigger the dismiss animation.
// Icon button
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
dismissButton = { onClick ->
IconButton(onClick = onClick) {
Icon(Icons.Default.Close, contentDescription = "I don't know")
}
}
)
// Image button
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
dismissButton = { onClick ->
Image(
painter = painterResource(Res.drawable.thumbs_down),
contentDescription = "I don't know",
modifier = Modifier.clickable(onClick = onClick)
)
}
)The unflip button is disabled by default (unflipButton = null). When enabled, it appears above the card (top-right) while the back face is visible. Tapping it plays the flip animation in reverse, returning to the front face without dismissing the card.
Enable it by supplying any composable — the onClick lambda must be called to trigger the animation:
CuteCard(
content = content,
onKnown = { ... },
onUnknown = { ... },
unflipButton = { onClick ->
IconButton(onClick = onClick) {
Icon(Icons.Default.Replay, contentDescription = "Show word")
}
}
)When null, no space is reserved above the card.
MIT — see LICENSE for details.
# Build all targets
./gradlew :cutecard:build
# Run common tests
./gradlew :cutecard:allTests
# Build Android artifact
./gradlew :cutecard:assemble
# Build iOS frameworks
./gradlew :cutecard:linkDebugFrameworkIosArm64 \
:cutecard:linkDebugFrameworkIosSimulatorArm64