suiss/uim · React Native Component
Keypad
A numeric keypad with round digit keys, a delete key and an optional decimal or biometric key.
Keypad is an open-source mobile component 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 * as React from "react"
import { View } from "react-native"
import { Keypad, type KeypadAccessoryKey, type KeypadKeyValue } from "@/components/ui/keypad"
import { Text } from "@/components/ui/text"
function KeyLog({
accessoryKey,
disabled,
}: {
accessoryKey?: KeypadAccessoryKey
disabled?: boolean
}) {
const [last, setLast] = React.useState<KeypadKeyValue | null>(null)
return (
<View className="items-center gap-6">
<Text
role="status"
accessibilityLiveRegion="polite"
className="text-sm text-label-secondary"
>
{last ? `Last key: ${last}` : "Press a key"}
</Text>
<Keypad
accessoryKey={accessoryKey}
disabled={disabled}
onKeyPress={setLast}
/>
</View>
)
}
export function KeypadExample() {
return (
<KeyLog />
)
}import EsuissUI
import SwiftUI
struct KeypadExample: View {
var body: some View {
KeypadLog(accessory: .none)
}
}
struct KeypadLog: View {
let accessory: KeypadAccessory
@State private var last: KeypadKey?
var body: some View {
VStack(spacing: 24) {
Group {
if let last {
Text(verbatim: "Last key: \(name(last))")
} else {
Text("Press a key")
}
}
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
Keypad(accessory: accessory) { key in
last = key
}
}
.frame(maxWidth: .infinity)
}
func name(_ key: KeypadKey) -> String {
switch key {
case .digit(let value): "\(value)"
case .decimal: "decimal"
case .delete: "delete"
case .biometric: "biometric"
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Keypad
import com.esuiss.ui.components.KeypadAccessory
import com.esuiss.ui.components.KeypadKey
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun KeypadExample() {
KeypadKeyLog(KeypadAccessory.None)
}
fun keypadKeyName(key: KeypadKey): String = when (key) {
is KeypadKey.Digit -> key.value.toString()
KeypadKey.Decimal -> "decimal"
KeypadKey.Biometric -> "biometric"
KeypadKey.Delete -> "delete"
}
@Composable
fun KeypadKeyLog(accessory: KeypadAccessory, enabled: Boolean = true) {
var last by rememberSaveable { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
last?.let { "Last key: $it" } ?: "Press a key",
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm,
color = EsuissTheme.colors.labelSecondary,
)
Keypad(onKeyPress = { last = keypadKeyName(it) }, accessory = accessory, enabled = enabled)
}
}Keypad lays out digits 1 to 9 in three rows and a bottom row with an accessory key, 0 and a delete key. Each key is a KeypadKey, a round Pressable with the button role. Digit keys have a filled background; the accessory and delete keys use the plain variant with no fill.
Use it for PIN entry, amounts and other numeric input where the system keyboard is not wanted.
Installation
$ pnpm dlx @esuiss/uim@latest add keypad$ pnpm dlx @esuiss/uim@latest add keypad --platform swift$ pnpm dlx @esuiss/uim@latest add keypad --platform kotlinThe CLI also adds what it builds on: Icon, Text.
Manual
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-svgimport { cn } from "cn"
import {
BackspaceIcon,
FingerprintIcon,
type Icon as PhosphorIcon,
} from "phosphor-react-native"
import type * as React from "react"
import { Pressable, View } from "react-native"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"
type KeypadDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
type KeypadKeyValue = KeypadDigit | "decimal" | "delete" | "biometric"
type KeypadAccessoryKey = "decimal" | "biometric"- Update the import paths to match your project.
- Install the dependencies:
npx expo install cn phosphor-react-native react-native-svgimport { cn } from "cn"
import {
BackspaceIcon,
FingerprintIcon,
type Icon as PhosphorIcon,
} from "phosphor-react-native"
import type * as React from "react"
import { Pressable, View } from "react-native"
import { Icon, IconClassContext } from "@/components/ui/icon"
import { Text, TextClassContext } from "@/components/ui/text"
type KeypadDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
type KeypadKeyValue = KeypadDigit | "decimal" | "delete" | "biometric"
type KeypadAccessoryKey = "decimal" | "biometric"- Update the import paths to match your project.
- Install the dependencies:
npx expo install phosphor-react-native react-native-svgimport {
BackspaceIcon,
FingerprintIcon,
type Icon as PhosphorIcon,
} from "phosphor-react-native"
import type * as React from "react"
import {
Pressable,
type StyleProp,
StyleSheet,
View,
type ViewStyle,
} from "react-native"
import { Icon, IconSizeContext } from "@/components/ui/icon"
import { Text, TextStyleContext } from "@/components/ui/text"
import { fontSize, radius, useTheme } from "@/components/ui/theme"- Update the import paths to match your project.
-
Target iOS 17 or later.
-
Add the Swift package to your app:
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")- 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 and every file it uses:
import PhosphorSwift
import SwiftUI
public enum KeypadKey: Hashable, Sendable {
case digit(Int)
case decimal
case delete
case biometric
}
public enum KeypadAccessory {
case none
case decimal(String, label: LocalizedStringKey = "Decimal separator")
case biometric(Image, label: LocalizedStringKey = "Use biometrics")
}import PhosphorSwift
import SwiftUI
extension Shadow {
static let control = Shadow(color: .black.opacity(0.08), radius: 0.5, y: 0.5)
static let thumb = Shadow(color: .black.opacity(0.12), radius: 1.5, y: 1)
}
struct SelectionIndicator<Mark: View>: View {
let checked: Bool
let pressed: Bool
let cornerRadius: CGFloat
let mark: Mark
@Environment(\.isEnabled) private var isEnabled
@Environment(\.isInvalid) private var invalidimport SwiftUI
extension EnvironmentValues {
@Entry public var isInvalid = false
@Entry var fieldInset = false
@Entry var fieldGroupInset = false
@Entry var fieldSetInset = false
}
extension View {
public func invalid(_ isInvalid: Bool = true) -> some View {
environment(\.isInvalid, isInvalid)
}
}
public enum FieldOrientation: CaseIterable, Sendable {import SwiftUI
extension EnvironmentValues {
@Entry public var hapticsEnabled = true
}
extension View {
public func haptics(_ enabled: Bool) -> some View {
environment(\.hapticsEnabled, enabled)
}
func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
}
func hapticFeedback<Trigger: Equatable>(import SwiftUI
public struct Separator: View {
let orientation: Axis
public init(_ orientation: Axis = .horizontal) {
self.orientation = orientation
}
public var body: some View {
Rectangle()
.fill(Color.separator)
.frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
.frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
.accessibilityHidden(true)
}import SwiftUI
public enum TextVariant: CaseIterable, Sendable {
case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted
var style: TextStyle {
switch self {
case .h1: .xl3
case .h2: .xl2
case .h3: .xl
case .h4, .lead, .large: .lg
case .code, .small, .muted: .sm
default: .base
}
}- 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")
implementation("com.adamglin:phosphor-icon-android:1.0.0")- 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.components
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector1D
import androidx.compose.animation.core.keyframes
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.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicTextpackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShapepackage com.esuiss.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutablepackage 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 androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
val LocalHapticsEnabled = staticCompositionLocalOf { true }
@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}package com.esuiss.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme
@Composable
fun Icon(
imageVector: ImageVector,- Update the
com.esuiss.uipackage names to match your project.
Usage
import { KeypadKey, Keypad } from "@/components/ui/keypad"<KeyLog />Handle input in onKeyPress, which receives a digit string ("0" to "9"), "decimal", "delete" or "biometric". The keypad holds no value of its own.
Set accessoryKey to decimal or biometric to fill the bottom-left slot; otherwise it stays empty. Change the decimal character with decimalSeparator and the biometric icon with biometricIcon.
Use deleteDisabled to turn off only the delete key, for example when the value is empty. KeypadKey is exported for custom layouts, along with the KeypadDigit, KeypadKeyValue and KeypadAccessoryKey types.
Examples
Decimal
A keypad with a decimal separator key.


import * as React from "react"
import { View } from "react-native"
import { Keypad, type KeypadAccessoryKey, type KeypadKeyValue } from "@/components/ui/keypad"
import { Text } from "@/components/ui/text"
function KeyLog({
accessoryKey,
disabled,
}: {
accessoryKey?: KeypadAccessoryKey
disabled?: boolean
}) {
const [last, setLast] = React.useState<KeypadKeyValue | null>(null)
return (
<View className="items-center gap-6">
<Text
role="status"
accessibilityLiveRegion="polite"
className="text-sm text-label-secondary"
>
{last ? `Last key: ${last}` : "Press a key"}
</Text>
<Keypad
accessoryKey={accessoryKey}
disabled={disabled}
onKeyPress={setLast}
/>
</View>
)
}
export function KeypadDecimalExample() {
return (
<KeyLog accessoryKey="decimal" />
)
}import EsuissUI
import SwiftUI
struct KeypadDecimalExample: View {
var body: some View {
KeypadLog(accessory: .decimal("."))
}
}
struct KeypadLog: View {
let accessory: KeypadAccessory
@State private var last: KeypadKey?
var body: some View {
VStack(spacing: 24) {
Group {
if let last {
Text(verbatim: "Last key: \(name(last))")
} else {
Text("Press a key")
}
}
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
Keypad(accessory: accessory) { key in
last = key
}
}
.frame(maxWidth: .infinity)
}
func name(_ key: KeypadKey) -> String {
switch key {
case .digit(let value): "\(value)"
case .decimal: "decimal"
case .delete: "delete"
case .biometric: "biometric"
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Keypad
import com.esuiss.ui.components.KeypadAccessory
import com.esuiss.ui.components.KeypadKey
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun KeypadDecimalExample() {
KeypadKeyLog(KeypadAccessory.Decimal())
}
fun keypadKeyName(key: KeypadKey): String = when (key) {
is KeypadKey.Digit -> key.value.toString()
KeypadKey.Decimal -> "decimal"
KeypadKey.Biometric -> "biometric"
KeypadKey.Delete -> "delete"
}
@Composable
fun KeypadKeyLog(accessory: KeypadAccessory, enabled: Boolean = true) {
var last by rememberSaveable { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
last?.let { "Last key: $it" } ?: "Press a key",
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm,
color = EsuissTheme.colors.labelSecondary,
)
Keypad(onKeyPress = { last = keypadKeyName(it) }, accessory = accessory, enabled = enabled)
}
}Biometric
A keypad with a biometric key.


import * as React from "react"
import { View } from "react-native"
import { Keypad, type KeypadAccessoryKey, type KeypadKeyValue } from "@/components/ui/keypad"
import { Text } from "@/components/ui/text"
function KeyLog({
accessoryKey,
disabled,
}: {
accessoryKey?: KeypadAccessoryKey
disabled?: boolean
}) {
const [last, setLast] = React.useState<KeypadKeyValue | null>(null)
return (
<View className="items-center gap-6">
<Text
role="status"
accessibilityLiveRegion="polite"
className="text-sm text-label-secondary"
>
{last ? `Last key: ${last}` : "Press a key"}
</Text>
<Keypad
accessoryKey={accessoryKey}
disabled={disabled}
onKeyPress={setLast}
/>
</View>
)
}
export function KeypadBiometricExample() {
return (
<KeyLog accessoryKey="biometric" />
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct KeypadBiometricExample: View {
var body: some View {
KeypadLog(accessory: .biometric(Ph.fingerprint.regular))
}
}
struct KeypadLog: View {
let accessory: KeypadAccessory
@State private var last: KeypadKey?
var body: some View {
VStack(spacing: 24) {
Group {
if let last {
Text(verbatim: "Last key: \(name(last))")
} else {
Text("Press a key")
}
}
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
Keypad(accessory: accessory) { key in
last = key
}
}
.frame(maxWidth: .infinity)
}
func name(_ key: KeypadKey) -> String {
switch key {
case .digit(let value): "\(value)"
case .decimal: "decimal"
case .delete: "delete"
case .biometric: "biometric"
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Keypad
import com.esuiss.ui.components.KeypadAccessory
import com.esuiss.ui.components.KeypadKey
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun KeypadBiometricExample() {
KeypadKeyLog(KeypadAccessory.Biometric())
}
fun keypadKeyName(key: KeypadKey): String = when (key) {
is KeypadKey.Digit -> key.value.toString()
KeypadKey.Decimal -> "decimal"
KeypadKey.Biometric -> "biometric"
KeypadKey.Delete -> "delete"
}
@Composable
fun KeypadKeyLog(accessory: KeypadAccessory, enabled: Boolean = true) {
var last by rememberSaveable { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
last?.let { "Last key: $it" } ?: "Press a key",
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm,
color = EsuissTheme.colors.labelSecondary,
)
Keypad(onKeyPress = { last = keypadKeyName(it) }, accessory = accessory, enabled = enabled)
}
}Disabled
A fully disabled keypad.


import * as React from "react"
import { View } from "react-native"
import { Keypad, type KeypadAccessoryKey, type KeypadKeyValue } from "@/components/ui/keypad"
import { Text } from "@/components/ui/text"
function KeyLog({
accessoryKey,
disabled,
}: {
accessoryKey?: KeypadAccessoryKey
disabled?: boolean
}) {
const [last, setLast] = React.useState<KeypadKeyValue | null>(null)
return (
<View className="items-center gap-6">
<Text
role="status"
accessibilityLiveRegion="polite"
className="text-sm text-label-secondary"
>
{last ? `Last key: ${last}` : "Press a key"}
</Text>
<Keypad
accessoryKey={accessoryKey}
disabled={disabled}
onKeyPress={setLast}
/>
</View>
)
}
export function KeypadDisabledExample() {
return (
<KeyLog accessoryKey="biometric" disabled />
)
}import EsuissUI
import PhosphorSwift
import SwiftUI
struct KeypadDisabledExample: View {
var body: some View {
KeypadLog(accessory: .biometric(Ph.fingerprint.regular))
.disabled(true)
}
}
struct KeypadLog: View {
let accessory: KeypadAccessory
@State private var last: KeypadKey?
var body: some View {
VStack(spacing: 24) {
Group {
if let last {
Text(verbatim: "Last key: \(name(last))")
} else {
Text("Press a key")
}
}
.textStyle(.sm)
.foregroundStyle(Color.labelSecondary)
.accessibilityAddTraits(.updatesFrequently)
Keypad(accessory: accessory) { key in
last = key
}
}
.frame(maxWidth: .infinity)
}
func name(_ key: KeypadKey) -> String {
switch key {
case .digit(let value): "\(value)"
case .decimal: "decimal"
case .delete: "delete"
case .biometric: "biometric"
}
}
}import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
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.LiveRegionMode
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Keypad
import com.esuiss.ui.components.KeypadAccessory
import com.esuiss.ui.components.KeypadKey
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography
@Composable
fun KeypadDisabledExample() {
KeypadKeyLog(KeypadAccessory.Biometric(), enabled = false)
}
fun keypadKeyName(key: KeypadKey): String = when (key) {
is KeypadKey.Digit -> key.value.toString()
KeypadKey.Decimal -> "decimal"
KeypadKey.Biometric -> "biometric"
KeypadKey.Delete -> "delete"
}
@Composable
fun KeypadKeyLog(accessory: KeypadAccessory, enabled: Boolean = true) {
var last by rememberSaveable { mutableStateOf<String?>(null) }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Text(
last?.let { "Last key: $it" } ?: "Press a key",
modifier = Modifier.semantics { liveRegion = LiveRegionMode.Polite },
style = Typography.sm,
color = EsuissTheme.colors.labelSecondary,
)
Keypad(onKeyPress = { last = keypadKeyName(it) }, accessory = accessory, enabled = enabled)
}
}API Reference
KeypadKey
A single round key that passes text and icon styles to its children.
React.ComponentProps<typeof Pressable>- About
defaultwith a filled background, orplainwithout one.- Type
"default""plain"- Default
- "default"
- Type
string- Default
- —
- Type
React.ReactNode- Default
- —
Keypad
The full grid of keys.
React.ComponentProps<typeof View>- About
- Called with the pressed key value.
- Type
(key: KeypadKeyValue) => void- Default
- —
- About
- Bottom-left key:
decimalorbiometric. - Type
KeypadAccessoryKey- Default
- —
- About
- Character shown on the decimal key. Defaults to
.. - Type
string- Default
- "."
- About
- Phosphor icon for the biometric key. Defaults to
FingerprintIcon. - Type
PhosphorIcon- Default
- FingerprintIcon
- About
- Accessibility label for the biometric key.
- Type
string- Default
- "Use biometrics"
- About
- Accessibility label for the decimal key.
- Type
string- Default
- "Decimal separator"
- About
- Accessibility label for the delete key.
- Type
string- Default
- "Delete"
- About
- Disables every key.
- Type
boolean- Default
- false
- About
- Disables only the delete key.
- Type
boolean- Default
- false
- About
- Extra classes applied to each key.
- Type
string- Default
- —
Accessibility
- Every key has the
buttonrole and anaccessibilityLabel: the digit itself, ordecimalLabel,biometricLabelanddeleteLabelfor the other keys. - Disabled keys set
disabledin their accessibility state and block presses. - Translate the default labels (
Decimal separator,Use biometrics,Delete) for localized apps.
Notes
- Filled keys use
bg-control,bg-control-pressedwhen pressed andbg-control-disabledwhen disabled. - Digits use
text-2xlwith tabular numbers. - Light and dark colors follow the system appearance.