Flat Checkbox
Alex Styl
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Image
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.style.animate
import androidx.compose.foundation.style.border
import androidx.compose.foundation.style.focused
import androidx.compose.foundation.style.hovered
import androidx.compose.foundation.style.rememberUpdatedStyleState
import androidx.compose.foundation.style.styleable
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.group
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composeunstyled.CheckedIndicator
import com.composeunstyled.TextInput
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledCheckbox
import com.composeunstyled.UnstyledIcon
import com.composeunstyled.UnstyledTextField
private val BodyTextStyle = TextStyle(color = Color.Black, fontSize = 14.sp)
private val FieldLabelTextStyle = TextStyle(color = Color(0xFF151717), fontWeight = FontWeight.SemiBold)
private val PasswordObfuscation = OutputTransformation { replace(0, length, "•".repeat(length)) }
@Preview
@Composable
fun SignInForm() {
val email = rememberTextFieldState()
val password = rememberTextFieldState()
var rememberMe by remember { mutableStateOf(false) }
var passwordVisible by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.width(450.dp)
.styleable {
shape(RoundedCornerShape(20.dp))
background(Color.White)
}
.padding(30.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
BasicText("Email", style = FieldLabelTextStyle)
StyledTextField(
state = email,
placeholder = "Enter your Email",
leadingIcon = EmailIcon,
)
BasicText("Password", style = FieldLabelTextStyle)
StyledTextField(
state = password,
placeholder = "Enter your Password",
leadingIcon = PasswordIcon,
trailingIcon = if (passwordVisible) EyeOffIcon else EyeIcon,
outputTransformation = if (passwordVisible) null else PasswordObfuscation,
onTrailingIconClick = { passwordVisible = !passwordVisible },
)
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
UnstyledCheckbox(
checked = rememberMe,
onCheckedChange = { rememberMe = it },
accessibilityLabel = "Remember me",
) {
CheckedIndicator(
modifier = Modifier
.height(18.dp)
.width(18.dp)
.styleable {
shape(RoundedCornerShape(4.dp))
border(1.dp, SolidColor(Color(0xFFECEDEC)))
background(Color.White)
},
) {
UnstyledIcon(imageVector = CheckIcon, tint = Color(0xFF151717))
}
}
BasicText("Remember me", style = BodyTextStyle)
}
TextLink("Forgot password?")
}
SubmitButton()
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
BasicText("Don't have an account?", style = BodyTextStyle)
TextLink("Sign Up")
}
BasicText(
text = "Or With",
modifier = Modifier.fillMaxWidth(),
style = BodyTextStyle.copy(textAlign = androidx.compose.ui.text.style.TextAlign.Center),
)
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
SocialButton(
label = "Google",
icon = GoogleLogo,
modifier = Modifier.weight(1f),
)
SocialButton(
label = "Apple",
icon = AppleIcon,
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun StyledTextField(
state: TextFieldState,
placeholder: String,
leadingIcon: ImageVector,
trailingIcon: ImageVector? = null,
outputTransformation: OutputTransformation? = null,
onTrailingIconClick: (() -> Unit)? = null,
) {
val interactionSource = remember { MutableInteractionSource() }
val styleState = rememberUpdatedStyleState(interactionSource)
UnstyledTextField(
state = state,
interactionSource = interactionSource,
lineLimits = TextFieldLineLimits.SingleLine,
outputTransformation = outputTransformation,
cursorBrush = SolidColor(Color(0xFF151717)),
textStyle = TextStyle(color = Color(0xFF151717), fontSize = 14.sp),
modifier = Modifier.fillMaxWidth(),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.styleable(styleState) {
shape(RoundedCornerShape(10.dp))
border(1.5.dp, SolidColor(Color(0xFFECEDEC)))
focused {
animate(tween(durationMillis = 200)) {
border(1.5.dp, SolidColor(Color(0xFF2D79F3)))
}
}
}
.padding(horizontal = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
UnstyledIcon(imageVector = leadingIcon, tint = Color(0xFF151717))
TextInput(
modifier = Modifier.weight(1f),
placeholder = {
BasicText(placeholder, style = TextStyle(color = Color(0xFF8D9191), fontSize = 14.sp))
},
)
trailingIcon?.let {
if (onTrailingIconClick == null) {
UnstyledIcon(imageVector = it, tint = Color(0xFF151717))
} else {
UnstyledButton(onClick = onTrailingIconClick, modifier = Modifier.size(24.dp)) {
UnstyledIcon(
imageVector = it,
tint = Color(0xFF151717),
modifier = Modifier.size(20.dp),
)
}
}
}
}
}
}
@Composable
private fun SubmitButton() {
val interactionSource = remember { MutableInteractionSource() }
val styleState = rememberUpdatedStyleState(interactionSource)
UnstyledButton(
onClick = {},
interactionSource = interactionSource,
indication = LocalIndication.current,
modifier = Modifier
.fillMaxWidth()
.padding(top = 10.dp, bottom = 8.dp)
.height(50.dp)
.styleable(styleState) {
clip()
shape(RoundedCornerShape(10.dp))
background(Color(0xFF151717))
hovered {
animate(tween(durationMillis = 200)) {
background(Color(0xFF252727))
}
}
},
) {
BasicText("Sign In", style = TextStyle(color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.Medium))
}
}
@Composable
private fun TextLink(text: String) {
UnstyledButton(onClick = {}) {
BasicText(
text = text,
modifier = Modifier.padding(start = 5.dp),
style = TextStyle(color = Color(0xFF2D79F3), fontSize = 14.sp, fontWeight = FontWeight.Medium),
)
}
}
@Composable
private fun SocialButton(
label: String,
icon: ImageVector,
modifier: Modifier,
) {
val interactionSource = remember { MutableInteractionSource() }
val styleState = rememberUpdatedStyleState(interactionSource)
UnstyledButton(
onClick = {},
interactionSource = interactionSource,
indication = LocalIndication.current,
modifier = modifier
.height(50.dp)
.styleable(styleState) {
clip()
shape(RoundedCornerShape(10.dp))
border(1.dp, SolidColor(Color(0xFFEDEDEF)))
background(Color.White)
hovered {
animate(tween(durationMillis = 200)) {
border(1.dp, SolidColor(Color(0xFF2D79F3)))
}
}
},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
Image(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp),
colorFilter = null,
)
BasicText(label, style = TextStyle(color = Color(0xFF151717), fontSize = 14.sp, fontWeight = FontWeight.Medium))
}
}
}
val GoogleLogo: ImageVector
get() {
if (_GoogleGLogo != null) return _GoogleGLogo!!
_GoogleGLogo = ImageVector.Builder(
name = "GoogleGLogo",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f
).apply {
path(
fill = SolidColor(Color(0xFF4285F4))
) {
moveTo(22.56f, 12.25f)
curveToRelative(0f, -0.78f, -0.07f, -1.53f, -0.2f, -2.25f)
horizontalLineTo(12f)
verticalLineToRelative(4.26f)
horizontalLineToRelative(5.92f)
curveToRelative(-0.26f, 1.37f, -1.04f, 2.53f, -2.21f, 3.31f)
verticalLineToRelative(2.77f)
horizontalLineToRelative(3.57f)
curveToRelative(2.08f, -1.92f, 3.28f, -4.74f, 3.28f, -8.09f)
close()
}
path(
fill = SolidColor(Color(0xFF34A853))
) {
moveTo(12f, 23f)
curveToRelative(2.97f, 0f, 5.46f, -0.98f, 7.28f, -2.66f)
lineToRelative(-3.57f, -2.77f)
curveToRelative(-0.98f, 0.66f, -2.23f, 1.06f, -3.71f, 1.06f)
curveToRelative(-2.86f, 0f, -5.29f, -1.93f, -6.16f, -4.53f)
horizontalLineTo(2.18f)
verticalLineToRelative(2.84f)
curveTo(3.99f, 20.53f, 7.7f, 23f, 12f, 23f)
close()
}
path(
fill = SolidColor(Color(0xFFFBBC05))
) {
moveTo(5.84f, 14.09f)
curveToRelative(-0.22f, -0.66f, -0.35f, -1.36f, -0.35f, -2.09f)
reflectiveCurveToRelative(0.13f, -1.43f, 0.35f, -2.09f)
verticalLineTo(7.07f)
horizontalLineTo(2.18f)
curveTo(1.43f, 8.55f, 1f, 10.22f, 1f, 12f)
reflectiveCurveToRelative(0.43f, 3.45f, 1.18f, 4.93f)
lineToRelative(2.85f, -2.22f)
lineToRelative(0.81f, -0.62f)
close()
}
path(
fill = SolidColor(Color(0xFFEA4335))
) {
moveTo(12f, 5.38f)
curveToRelative(1.62f, 0f, 3.06f, 0.56f, 4.21f, 1.64f)
lineToRelative(3.15f, -3.15f)
curveTo(17.45f, 2.09f, 14.97f, 1f, 12f, 1f)
curveTo(7.7f, 1f, 3.99f, 3.47f, 2.18f, 7.07f)
lineToRelative(3.66f, 2.84f)
curveToRelative(0.87f, -2.6f, 3.3f, -4.53f, 6.16f, -4.53f)
close()
}
}.build()
return _GoogleGLogo!!
}
private var _GoogleGLogo: ImageVector? = null
private val EmailIcon: ImageVector
get() {
if (_EmailIcon != null) return _EmailIcon!!
_EmailIcon = ImageVector.Builder(
name = "EmailIcon",
defaultWidth = 20.dp,
defaultHeight = 20.dp,
viewportWidth = 32f,
viewportHeight = 32f,
).apply {
path(fill = SolidColor(Color(0xFF151717))) {
moveTo(30.853f, 13.87f)
arcToRelative(15f, 15f, 0f, false, false, -29.729f, 4.082f)
arcToRelative(15.1f, 15.1f, 0f, false, false, 12.876f, 12.918f)
arcToRelative(15.6f, 15.6f, 0f, false, false, 2.016f, 0.13f)
arcToRelative(14.85f, 14.85f, 0f, false, false, 7.715f, -2.145f)
arcToRelative(1f, 1f, 0f, true, false, -1.031f, -1.711f)
arcToRelative(13.007f, 13.007f, 0f, true, true, 5.458f, -6.529f)
arcToRelative(2.149f, 2.149f, 0f, false, true, -4.158f, -0.759f)
verticalLineTo(9f)
arcToRelative(1f, 1f, 0f, false, false, -2f, 0f)
verticalLineToRelative(1.726f)
arcToRelative(8f, 8f, 0f, true, false, 0.2f, 10.325f)
arcToRelative(4.135f, 4.135f, 0f, false, false, 7.83f, 0.274f)
arcToRelative(15.2f, 15.2f, 0f, false, false, 0.823f, -7.455f)
close()
moveTo(16f, 22f)
arcToRelative(6f, 6f, 0f, true, true, 6f, -6f)
arcTo(6.006f, 6.006f, 0f, false, true, 16f, 22f)
close()
}
}.build()
return _EmailIcon!!
}
private var _EmailIcon: ImageVector? = null
private val PasswordIcon: ImageVector
get() {
if (_PasswordIcon != null) return _PasswordIcon!!
_PasswordIcon = ImageVector.Builder(
name = "PasswordIcon",
defaultWidth = 20.dp,
defaultHeight = 20.dp,
viewportWidth = 512f,
viewportHeight = 512f,
).apply {
group(translationX = 64f) {
path(fill = SolidColor(Color(0xFF151717))) {
moveTo(336f, 512f)
horizontalLineTo(48f)
arcToRelative(48f, 48f, 0f, false, true, -48f, -48f)
verticalLineTo(240f)
arcToRelative(48f, 48f, 0f, false, true, 48f, -48f)
horizontalLineTo(336f)
arcToRelative(48f, 48f, 0f, false, true, 48f, 48f)
verticalLineTo(464f)
arcToRelative(48f, 48f, 0f, false, true, -48f, 48f)
close()
moveTo(48f, 224f)
arcToRelative(16f, 16f, 0f, false, false, -16f, 16f)
verticalLineTo(464f)
arcToRelative(16f, 16f, 0f, false, false, 16f, 16f)
horizontalLineTo(336f)
arcToRelative(16f, 16f, 0f, false, false, 16f, -16f)
verticalLineTo(240f)
arcToRelative(16f, 16f, 0f, false, false, -16f, -16f)
close()
}
path(fill = SolidColor(Color(0xFF151717))) {
moveTo(304f, 224f)
arcToRelative(16f, 16f, 0f, false, true, -16f, -16f)
verticalLineTo(128f)
arcToRelative(96f, 96f, 0f, false, false, -192f, 0f)
verticalLineTo(208f)
arcToRelative(16f, 16f, 0f, true, true, -32f, 0f)
verticalLineTo(128f)
arcToRelative(128f, 128f, 0f, false, true, 256f, 0f)
verticalLineTo(208f)
arcToRelative(16f, 16f, 0f, false, true, -16f, 16f)
close()
}
}
}.build()
return _PasswordIcon!!
}
private var _PasswordIcon: ImageVector? = null
/*
* Eye icons from Lucide (lucide), licensed under ISC.
* https://lucide.dev/license
*/
private val EyeIcon = lucideIcon("Eye") {
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round,
strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round,
) {
moveTo(2.062f, 12.348f)
arcToRelative(1f, 1f, 0f, false, true, 0f, -0.696f)
arcToRelative(10.75f, 10.75f, 0f, false, true, 19.876f, 0f)
arcToRelative(1f, 1f, 0f, false, true, 0f, 0.696f)
arcToRelative(10.75f, 10.75f, 0f, false, true, -19.876f, 0f)
}
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round,
strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round,
) {
moveTo(15f, 12f)
arcTo(3f, 3f, 0f, false, true, 12f, 15f)
arcTo(3f, 3f, 0f, false, true, 9f, 12f)
arcTo(3f, 3f, 0f, false, true, 15f, 12f)
close()
}
}
private val EyeOffIcon = lucideIcon("EyeOff") {
path(fill = SolidColor(Color.Transparent), stroke = SolidColor(Color.Black), strokeLineWidth = 2f, strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round, strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round) {
moveTo(10.733f, 5.076f)
arcToRelative(10.744f, 10.744f, 0f, false, true, 11.205f, 6.575f)
arcToRelative(1f, 1f, 0f, false, true, 0f, 0.696f)
arcToRelative(10.747f, 10.747f, 0f, false, true, -1.444f, 2.49f)
}
path(fill = SolidColor(Color.Transparent), stroke = SolidColor(Color.Black), strokeLineWidth = 2f, strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round, strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round) {
moveTo(14.084f, 14.158f)
arcToRelative(3f, 3f, 0f, false, true, -4.242f, -4.242f)
}
path(fill = SolidColor(Color.Transparent), stroke = SolidColor(Color.Black), strokeLineWidth = 2f, strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round, strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round) {
moveTo(17.479f, 17.499f)
arcToRelative(10.75f, 10.75f, 0f, false, true, -15.417f, -5.151f)
arcToRelative(1f, 1f, 0f, false, true, 0f, -0.696f)
arcToRelative(10.75f, 10.75f, 0f, false, true, 4.446f, -5.143f)
}
path(fill = SolidColor(Color.Transparent), stroke = SolidColor(Color.Black), strokeLineWidth = 2f, strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round, strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round) {
moveTo(2f, 2f)
lineToRelative(20f, 20f)
}
}
private fun lucideIcon(name: String, content: ImageVector.Builder.() -> Unit): ImageVector =
ImageVector.Builder(name, 24.dp, 24.dp, 24f, 24f).apply(content).build()
private val AppleIcon: ImageVector
get() {
if (_AppleIcon != null) return _AppleIcon!!
_AppleIcon = ImageVector.Builder(
name = "AppleIcon",
defaultWidth = 20.dp,
defaultHeight = 20.dp,
viewportWidth = 22.773f,
viewportHeight = 22.773f,
).apply {
path(fill = SolidColor(Color.Black)) {
moveTo(15.769f, 0f)
curveToRelative(0.053f, 0f, 0.106f, 0f, 0.162f, 0f)
curveToRelative(0.13f, 1.606f, -0.483f, 2.806f, -1.228f, 3.675f)
curveToRelative(-0.731f, 0.863f, -1.732f, 1.7f, -3.351f, 1.573f)
curveToRelative(-0.108f, -1.583f, 0.506f, -2.694f, 1.25f, -3.561f)
curveToRelative(0.69f, -0.808f, 1.955f, -1.527f, 3.167f, -1.687f)
close()
moveTo(20.67f, 16.716f)
curveToRelative(0f, 0.016f, 0f, 0.03f, 0f, 0.045f)
curveToRelative(-0.455f, 1.378f, -1.104f, 2.559f, -1.896f, 3.655f)
curveToRelative(-0.723f, 0.995f, -1.609f, 2.334f, -3.191f, 2.334f)
curveToRelative(-1.367f, 0f, -2.275f, -0.879f, -3.676f, -0.903f)
curveToRelative(-1.482f, -0.024f, -2.297f, 0.735f, -3.652f, 0.926f)
horizontalLineTo(7.793f)
curveToRelative(-0.995f, -0.144f, -1.798f, -0.932f, -2.383f, -1.642f)
curveToRelative(-1.725f, -2.098f, -3.058f, -4.808f, -3.306f, -8.276f)
verticalLineTo(11.836f)
curveToRelative(0.105f, -2.482f, 1.311f, -4.5f, 2.914f, -5.478f)
curveToRelative(0.846f, -0.52f, 2.009f, -0.963f, 3.304f, -0.765f)
curveToRelative(0.555f, 0.086f, 1.122f, 0.276f, 1.619f, 0.464f)
curveToRelative(0.471f, 0.181f, 1.06f, 0.502f, 1.618f, 0.485f)
curveToRelative(0.378f, -0.011f, 0.754f, -0.208f, 1.135f, -0.347f)
curveToRelative(1.116f, -0.403f, 2.21f, -0.865f, 3.652f, -0.648f)
curveToRelative(1.733f, 0.262f, 2.963f, 1.032f, 3.723f, 2.22f)
curveToRelative(-1.466f, 0.933f, -2.625f, 2.339f, -2.427f, 4.74f)
curveToRelative(0.075f, 2.181f, 1.343f, 3.457f, 2.927f, 4.209f)
close()
}
}.build()
return _AppleIcon!!
}
private var _AppleIcon: ImageVector? = null
private val CheckIcon: ImageVector
get() {
if (_CheckIcon != null) return _CheckIcon!!
_CheckIcon = ImageVector.Builder(
name = "CheckIcon",
defaultWidth = 24.dp,
defaultHeight = 24.dp,
viewportWidth = 24f,
viewportHeight = 24f,
).apply {
path(
fill = SolidColor(Color.Transparent),
stroke = SolidColor(Color.Black),
strokeLineWidth = 2f,
strokeLineCap = androidx.compose.ui.graphics.StrokeCap.Round,
strokeLineJoin = androidx.compose.ui.graphics.StrokeJoin.Round,
) {
moveTo(20f, 6f)
lineTo(9f, 17f)
lineToRelative(-5f, -5f)
}
}.build()
return _CheckIcon!!
}
private var _CheckIcon: ImageVector? = null
implementation("com.composables:composeunstyled")