In this tutorial, we’ll build a Flutter sign in screen with a sign-up option and smooth animations — all without using external packages. Want a slick, animated sign-in / sign-up card in Flutter without adding any external libraries? This tutorial shows how to build one from scratch and explains every part of the code you provided — line by line — so you understand how the animations, layout, and interactions work. Wherever the UI uses a brand string in the original code, use “web dev service” instead.
What you’ll learn in flutter sign in
- How to create animated sign-in and sign-up containers using only
AnimationController
,Tween
s andAnimatedBuilder
. - How to coordinate multiple animations (size, shape, opacity) to create a polished transition.
- A full, readable explanation of each part of your code so you can adapt it or extend it.
Flutter Sign In Screen UI Overview
This UI is a Stack
with three main pieces layered:
- a faint clone card (shadow/ghost) for depth,
- the main sign-in card with animated button,
- a sign-up card that expands and slides in when triggered.
Two AnimationController
s drive the motion:
singInController
animates the sign-in button (shrinking label, expanding confirmation circle).animationController
animates the sign-up side panel (height, width, extra padding etc).
AnimatedBuilder
widgets rebuild small parts of the UI when animation values change — efficient and expressive with no extra packages.
Line-by-line explanation of your code
I’ll group related lines together for clarity. When I mention literal UI text that the app displays (e.g. "SmartKit"
in your source), swap it to “web dev service” as you requested.
Imports and color constants
import 'package:flutter/material.dart';
// Colors for app
const Color primary = Color(0xff3e66ff);
const Color secondary = Color(0xff1f1567);
const Color fontColor = Color(0xff79749c);
const Color lightWhite1 = Color(0xffEEF2F9);
import 'package:flutter/material.dart';
: brings in Flutter material components and UI classes.primary
,secondary
,fontColor
,lightWhite1
: named immutable colors used throughout the UI so styling is centralized.
Widget declaration for flutter sign in
class SignInFormScreen extends StatefulWidget {
const SignInFormScreen({Key? key}) : super(key: key);
@override
State<SignInFormScreen> createState() => _SignInFormScreenState();
}
- Declares a
StatefulWidget
because the UI has animations and internal state that change over time. createState()
returns the mutable state object_SignInFormScreenState
.
State class and basic fields for flutter sign in
class _SignInFormScreenState extends State<SignInFormScreen>
with TickerProviderStateMixin {
double? width, height;
bool singupContantShow = false;
bool widthforsingup = false;
_SignInFormScreenState
holds the animation controllers and UI building logic.with TickerProviderStateMixin
: providesvsync
signals for animation controllers (saves resources).width
andheight
store screen dimensions (filled inbuild()
).singupContantShow
toggles whether the sign-up content is shown inside the sign-up card.widthforsingup
is a flag used in layout math for the sign-up container (controls additional sizing logic).
Sign-in button animation controller & tweens for flutter sign in
late final AnimationController singInController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 300));
late final Animation<double> FontSizeAnimation =
Tween<double>(begin: 14, end: 0).animate(CurvedAnimation(
parent: singInController,
curve: const Interval(0.0, 0.30, curve: Curves.linear)));
late final Animation<double> CircleAnimation =
Tween<double>(begin: 0, end: 30).animate(CurvedAnimation(
parent: singInController,
curve: const Interval(0.70, 1, curve: Curves.linear)));
late final Animation<double> DoneIconAnimation =
Tween<double>(begin: 0, end: 500).animate(CurvedAnimation(
parent: singInController,
curve: const Interval(0.0, 1.0, curve: Curves.linear)));
late final Animation<double> BorderAnimation =
Tween<double>(begin: 25, end: 10).animate(CurvedAnimation(
parent: singInController,
curve: const Interval(0.0, 1.0, curve: Curves.linear)));
singInController
: drives the sign-in action animation (300 ms).FontSizeAnimation
: animates the “SIGN IN” label font from 14 → 0 during first 30% of the controller timeline — this makes the text disappear early.CircleAnimation
: animates a circle size from 0 → 30 near the end (70–100%) — used for a confirmation icon size change.DoneIconAnimation
: animates a large container size (0 → 500) across the whole timeline — this is used to grow a colored rounded shape behind the icon to create a “fill” effect.BorderAnimation
: animates border radius for that growing shape (from 25 to 10) for a subtle shape change.
These are coordinated using Interval
s so different effects happen at different moments.
Sign-up container animation controller & tweens
late final AnimationController animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
// for height
late final Animation<double> firstHeightAnimation =
Tween<double>(begin: 0.22, end: 0.45).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 0.75, curve: Curves.linear)));
late final Animation<double> secondHeightAnimation =
Tween<double>(begin: 0.0, end: 1.0).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.75, 1.0, curve: Curves.linear)));
// for width
late final Animation<double> widthAnimation =
Tween<double>(begin: 0.0, end: 0.50).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 0.50, curve: Curves.easeIn)));
late final Animation<double> secondWidthAnimation =
Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 0.99, curve: Curves.easeIn)));
late final Animation<double> containerAnimation =
Tween<double>(begin: 1, end: 5).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.5, 1, curve: Curves.ease)));
// extra Height
late final Animation<double> extraHightForSingupAnimation =
Tween<double>(begin: 0, end: 100).animate(CurvedAnimation(
parent: animationController,
curve: const Interval(0.7, 1, curve: Curves.ease)));
animationController
: drives the sign-up panel opening/closing (500 ms).firstHeightAnimation
andsecondHeightAnimation
combine to produce a two-stage vertical transform. The first adjusts top padding or base height; the second toggles a further factor (used as 0→1 to control final expanded state).widthAnimation
andsecondWidthAnimation
control horizontal slide/offset / width of the sign-up card.containerAnimation
scales a factor used to compute the sign-up cardwidth
andheight
, producing a noticeable expansion.extraHightForSingupAnimation
adds an extra pixel height (0 → 100) at the end of the animation so the sign-up form gains vertical space once fully expanded.
All these cooperating tweens let you choreograph a multi-dimensional expansion of the sign-up card.
dispose()
@override
void dispose() {
singInController.dispose();
animationController.dispose();
super.dispose();
}
- Properly disposes both animation controllers when the state is removed, freeing resources for flutter sign in.
getCloneContainer()
getCloneContainer() {
return Positioned(
top: height! * 0.25,
left: width! * 0.075,
child: Opacity(
opacity: 0.5,
child: Container(
width: width! * 0.85,
height: height! * 0.61,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white),
color: Colors.white,
),
),
),
);
}
- Builds a semi-transparent white card behind the main card to produce depth.
Positioned
uses screenwidth
andheight
to place this element relative to screen size (responsive-ish).Opacity(opacity: 0.5)
makes it feel like a shadow or ghost duplicate for flutter sign in.
getMainContainer()
This is the long function that builds the main sign-in card UI. I’ll highlight the key parts rather than repeating every TextFormField
block.
Top wrapper:
Positioned(
top: height! * 0.25,
left: width! * 0.05,
child: Opacity(
opacity: 1,
child: Container(
padding: const EdgeInsets.all(30.0),
width: width! * 0.9,
height: height! * 0.59,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
border: Border.all(color: Colors.white),
color: Colors.white,
),
child: SingleChildScrollView(
child: Column(
...
),
),
),
),
),
- Another
Positioned
card placed slightly inset from the clone. SingleChildScrollView
ensures the form scrolls if vertical space is limited (useful on smaller screens / keyboards).
Header row:
Row(
children: const [
Icon(Icons.radio_button_checked, color: primary),
SizedBox(width: 5),
Text("Web Dev Services", style: TextStyle(color: primary, fontSize: 20, fontWeight: FontWeight.bold))
],
),
Form fields (email/password) use identical patterns:
- Box
Container
with border,TextFormField
inside. TextFormField
hassuffixIcon
,keyboardType
, andmaxLines: 1
.
Forgot password row:
- Simple
Row
aligned to end with a grey text hint.
Sign-in button (AnimatedBuilder)
Key block:
AnimatedBuilder(
animation: singInController,
builder: (context, child) {
return InkWell(
onTap: () {
if (singInController.isCompleted) {
singInController
.reverse()
.then((value) => print("Animation reversed"));
} else {
singInController.forward();
}
},
child: Container(
decoration: BoxDecoration(
color: secondary,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: fontColor,
width: 1,
),
),
height: 50,
width: width,
child: Stack(
children: [
Center(
child: Container(
width: DoneIconAnimation.value,
height: DoneIconAnimation.value,
decoration: BoxDecoration(
color: primary,
borderRadius: BorderRadius.circular(BorderAnimation.value),
),
),
),
Center(
child: Icon(
Icons.done,
size: CircleAnimation.value,
color: Colors.white,
),
),
Center(
child: Text(
"SIGN IN",
style: TextStyle(
color: Colors.white,
fontSize: FontSizeAnimation.value,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}),
AnimatedBuilder
rebuilds this button whensingInController
ticks for flutter sign in.InkWell
wraps the button to capture taps.- On tap: toggle
singInController
forward/reverse to play the animation. - The button is a
Stack
with three stacked center children:- a growing rounded
Container
whose size isDoneIconAnimation.value
and border radius is animated viaBorderAnimation
. This produces a colored fill that expands from center. - an
Icon
whosesize
is driven byCircleAnimation
— it appears/grows near the end. - the label
"SIGN IN"
whosefontSize
is animated down to 0 viaFontSizeAnimation
— so the label disappears early while the fill grows behind it.
- a growing rounded
- The interplay produces: label fades/shrinks → background colored shape expands → check icon grows into view for flutter sign in..
Notes:
- The label text is never actually faded using opacity; instead its
fontSize
collapses to zero. This is fine but you can combine opacity with size for smoother results for flutter sign in.
Social icons row
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// three image icons (google, linkedin-ish image, twitter)
],
),
- Simple row showing network login icons via
Image.network
. Works for prototype; for production prefer packaged assets and legal attribution for flutter sign in.
Small helpers
getSizedBox(double valueHigh, double valueWidh) {
return SizedBox(
height: valueHigh,
width: valueHigh,
);
}
getText(String title, Color color) {
return Text(
title,
style: TextStyle(
color: color,
),
);
}
getSizedBox
returns aSizedBox
; note the second param namevalueWidh
is unused — you always returnwidth: valueHigh
. If you intended separate height/width, update this for flutter sign in.getText
returns a simple styledText
widget to reuse label style.
signUpContainer() (animated sign-up panel)
This method returns an AnimatedBuilder
driven by animationController
. It contains logic that inspects secondHeightAnimation.status
and sets local booleans to control inner layout:
if (secondHeightAnimation.status.toString() ==
"AnimationStatus.dismissed") {
singupContantShow = false;
widthforsingup = true;
}
...
if (secondHeightAnimation.status.toString() ==
"AnimationStatus.completed") {
singupContantShow = true;
widthforsingup = true;
}
...
- These
if
blocks check the animation status string and setsingupContantShow
so the inner content toggles at the right moment. A cleaner pattern is attaching a listener to the controller and callingsetState()
when status changes, but checking inside the builder also works because the builder executes every frame. (One caveat: compare enum values directly — e.g.secondHeightAnimation.status == AnimationStatus.completed
— rather than stringifying.)
Compute positions and sizes:
final topPaddingPercentage =
firstHeightAnimation.value - 0.23 * secondHeightAnimation.value;
final leftPaddingPercentage =
widthAnimation.value - 0.5 * secondWidthAnimation.value;
- These arithmetic expressions combine both animations to create a two-phase motion: initial slide/resize and final lock position.
The returned Positioned
has:
top: height! * topPaddingPercentage
: controls vertical position.right: width! * leftPaddingPercentage
: controls horizontal offset from right.- The inner
Container
has:padding
set to30.0
only whensingupContantShow
is true (so content is inserted after expansion).width: (width! * (0.2 * containerAnimation.value)) - (singupContantShow ? 20 : 0)
: usescontainerAnimation
to scale width from small → large; subtracts a bit when content is visible.height: height! * (0.1 * containerAnimation.value) + (widthforsingup ? extraHightForSingupAnimation.value : 0)
: similarly scales height and adds extra vertical space when the flag is true.
Inside, when singupContantShow
is true
, the function builds the full sign-up form: title, username, email, password fields, and a white “SIGN IN” button (this should probably read “SIGN UP” — check the intended verb). When singupContantShow
is false
, it shows a single Icon
(three lines) that acts as a toggle to open the panel.
Interaction:
InkWell(
onTap: () {
if (animationController.isCompleted) {
animationController.reverse().then((value) => print("Animation reversed"));
} else {
animationController.forward();
}
},
child: const Icon(Icons.close, color: Colors.white, size: 45),
),
- Tapping the close icon (or the small
Icons.subject
when collapsed) toggles theanimationController
forward/reverse.
build()
@override
Widget build(BuildContext context) {
width = MediaQuery.of(context).size.width;
height = MediaQuery.of(context).size.height;
return Scaffold(
backgroundColor: lightWhite1,
resizeToAvoidBottomInset: false,
body: Stack(
children: [
Container(
width: width,
height: height,
color: lightWhite1,
),
getCloneContainer(),
getMainContainer(),
singUpContainer(),
],
),
);
}
- Grabs
width
andheight
from the screen soPositioned
widgets can be responsive. Scaffold
withbackgroundColor
.resizeToAvoidBottomInset: false
prevents the scaffold from resizing when the keyboard appears — usefulness depends on the UX you want. If fields are obscured by keyboard, consider setting totrue
or usingSingleChildScrollView
appropriately.Stack
layers the clone, main sign-in card, and the sign-up container on top.
Practical tips & improvements
- Use enum checks instead of strings
ReplacesecondHeightAnimation.status.toString() == "AnimationStatus.completed"
withsecondHeightAnimation.status == AnimationStatus.completed
— it’s safer, clearer, and avoids fragile string comparisons. - Avoid storing
width
andheight
as nullable fields
Instead computefinal width = MediaQuery.of(context).size.width;
insidebuild
and pass them to the builder methods as parameters or keep themlate
non-nullable after assignment. - Accessibility & labels
Replace hardcoded placeholder strings with localized strings. Add semantic labels for icons and fields. - Keyboard handling
resizeToAvoidBottomInset: false
may cause fields to be hidden by keyboard on small screens. Considertrue
or usingMediaQuery.of(context).viewInsets.bottom
to add padding when the keyboard is open. - Button animation polish
Combine opacity and size transforms for smoother appearance. For example wrap the label inOpacity(opacity: someValue)
and animatesomeValue
from 1 → 0. - Use
setState
only when necessary
YourAnimatedBuilder
rebuilds only the animated part — great. Avoid callingsetState()
frequently for animation frames (letAnimatedBuilder
handle rebuilds). - Rename
getSizedBox
and fix width param
The function currently ignores the second parameter. If you need variable width, use both args properly. - Production images
Do not hotlink icons from random web URLs — bundle them as assets for reliability and performance. - Fix label inconsistency
The sign-up card’s button says"SIGN IN"
— change to"SIGN UP"
if needed.
Minimal checklist to make it production-ready
- Replace literal brand text with
"web dev service"
everywhere. - External image → in-app assets.
- Replace string-based animation status checks.
- Make helpers typed (
Widget getCloneContainer(double w, double h)
etc) or passwidth
/height
explicitly to avoid nullable fields. - Add input controllers, validators, and actual authentication callbacks.
Short conclusion + how the pieces fit together
Coordinate animations with Interval
s to choreograph staged transitions — text shrinks, background expands, icon grows.
AnimationController
+ Tween
s + AnimatedBuilder
are powerful enough to create complex, polished UI without extra packages.
Keep animation responsibilities isolated: one controller per cohesive animation sequence (button vs panel).
Compose small, reusable builders (getMainContainer
, signUpContainer
) to keep build()
readable.