Sistema de Login Multi-Tema Animado en JavaFX

Sube el nivel de tus aplicaciones JavaFX con una pantalla de Login profesional. Incluye animaciones de error (Shake), transiciones suaves y cambio de tema dinámico en tiempo real.

J

JJ Arroyo

3 de marzo de 2026 9 min de lectura

Sistema de Login Multi-Tema Animado en JavaFX

Una pantalla de autenticación no debería ser solo dos cajas de texto aburridas. Es la primera impresión que tienen los usuarios de tu software. En este proyecto construiremos un Sistema de Login Animado robusto y visualmente deslumbrante usando JavaFX.

¿Qué vamos a implementar?

  1. Animación de Error ("Shake"): Cuando el usuario ingrese mal la contraseña, la ventana vibrará igual que lo hace macOS o Windows.
  2. Fade Transitions: Un fundido suave para transicionar a la pantalla "Dashboard" una vez que el login sea exitoso.
  3. Persistencia (Recordar Usuario): Usaremos java.util.prefs.Preferences para guardar el nombre de usuario localmente.
  4. Temas Dinámicos: Cambiaremos el archivo .css en vivo desde un ComboBox (Light SaaS, Dark Glass y Neumorphism).

1. La Interfaz (FXML) y el Selector de Temas

Nuestra vista principal (LoginView.fxml) utiliza un StackPane como base. Esto nos permite poner la tarjeta de Login centrada y añadir un menú flotante en la esquina superior derecha para el selector de temas.

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import javafx.collections.FXCollections?>
<?import java.lang.String?>

<StackPane fx:id="rootPane" styleClass="main-bg" stylesheets="@../css/style-light.css" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.login_animado.controller.LoginController">

    <!-- Selector de Tema (Arriba a la derecha) -->
    <HBox alignment="TOP_RIGHT" spacing="10" StackPane.alignment="TOP_RIGHT">
        <padding><Insets top="20" right="20" /></padding>
        <Label text="Estilo:" styleClass="theme-label" />
        <ComboBox fx:id="themeComboBox" styleClass="theme-combo" onAction="#handleThemeSwitch">
            <items>
                <FXCollections fx:factory="observableArrayList">
                    <String fx:value="Light SaaS" />
                    <String fx:value="Dark Glass" />
                    <String fx:value="Neumorphism" />
                </FXCollections>
            </items>
            <value><String fx:value="Light SaaS" /></value>
        </ComboBox>
    </HBox>

    <!-- Tarjeta de Login -->
    <VBox fx:id="loginCard" styleClass="login-card" alignment="CENTER" spacing="25" maxWidth="400" maxHeight="500">
        <StackPane.margin><Insets top="40" /></StackPane.margin>
        <padding><Insets top="40" right="40" bottom="40" left="40" /></padding>

        <VBox alignment="CENTER" spacing="10">
            <Circle radius="35" styleClass="logo-circle" />
            <Label text="Bienvenido" styleClass="title-label" />
            <Label text="Ingresa a tu cuenta para continuar" styleClass="subtitle-label" />
        </VBox>

        <VBox spacing="15" VBox.vgrow="ALWAYS">
            <VBox spacing="5">
                <Label text="Usuario" styleClass="input-label" />
                <TextField fx:id="txtUser" promptText="ej. admin" styleClass="input-field" />
            </VBox>

            <VBox spacing="5">
                <Label text="Contraseña" styleClass="input-label" />
                <PasswordField fx:id="txtPassword" promptText="••••••••" styleClass="input-field" />
            </VBox>

            <CheckBox fx:id="chkRememberMode" text="Recordar usuario" styleClass="check-box" />
        </VBox>

        <Label fx:id="lblError" text="Error de credenciales" styleClass="error-label" visible="false" managed="false" />
        <Button text="Iniciar Sesión" onAction="#handleLogin" styleClass="btn-primary" maxWidth="Infinity" />
    </VBox>
</StackPane>

2. Los Múltiples Temas (CSS)

La magia de poder cambiar de tema en tiempo real radica en separar completamente la lógica de los estilos. Hemos creado tres archivos en src/main/resources/css/.

[!TIP] Si te interesa entender a fondo cómo funcionan las sombras del tema Neumorphism, revisa nuestro artículo dedicado a los Botones Neumórficos en JavaFX.

Tema 1: Light SaaS

Un diseño ultra-limpio, fondos blancos, bordes sutiles y botones azules prominentes. Perfecto para aplicaciones B2B. Light SaaS Theme

Tema 2: Dark Glass

Elegante gradiente oscuro con transparencias (rgba) que interactúan entre la ventana flotante y el fondo oscuro. Dark Glassmorphism Theme

Tema 3: Neumorphism

Simula plástico moldeado por inyección. Para este CSS utilizamos combinaciones complejas de dropshadow e innershadow. Neumorphism Theme

3. El Controlador: Animación y Guardado Local

Aquí es donde atamos todo:

  1. Cambio de Tema: Limpiamos la lista de estilos (rootPane.getStylesheets().clear()) y cargamos el nuevo CSS correspondiente al valor del ComboBox.
  2. Animación "Shake": Instanciamos una TranslateTransition de apenas 50 milisegundos que mueve la tarjeta en el eje X (setByX(10f)), repetida 6 veces.
  3. Animación Fade: Usamos FadeTransition en el nodo raíz para hacerlo desaparecer (1.0 a 0.0), y en el evento setOnFinished(...) cargamos el FXML del Dashboard y lo hacemos aparecer (0.0 a 1.0).
  4. Java Preferences: java.util.prefs.Preferences salva pequeños datos en el registro (Windows) o archivos ocultos (Mac/Linux) automáticamente sin lidiar con .txt o bases de datos SQLite para pequeñeces como "Recordar el usuario".
package com.login_animado.controller;

import javafx.animation.FadeTransition;
import javafx.animation.TranslateTransition;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import java.io.IOException;
import java.util.prefs.Preferences;

public class LoginController {

    @FXML private StackPane rootPane;
    @FXML private VBox loginCard;
    @FXML private ComboBox<String> themeComboBox;
    @FXML private TextField txtUser;
    @FXML private PasswordField txtPassword;
    @FXML private CheckBox chkRememberMode;
    @FXML private Label lblError;

    // Constantes para Local Storage de Java
    private Preferences prefs;
    private static final String PREF_REMEMBER = "remember_user";
    private static final String PREF_USER = "saved_user";

    @FXML
    public void initialize() {
        prefs = Preferences.userNodeForPackage(LoginController.class);

        // Cargar preferencias guardadas la última vez
        boolean remember = prefs.getBoolean(PREF_REMEMBER, false);
        chkRememberMode.setSelected(remember);
        if (remember) {
            String savedUser = prefs.get(PREF_USER, "");
            txtUser.setText(savedUser);
            // Salta el foco a la contraseña ya que el usuario está lleno
            Platform.runLater(() -> txtPassword.requestFocus());
        }
    }

    @FXML
    protected void handleThemeSwitch(ActionEvent event) {
        String theme = themeComboBox.getValue();
        rootPane.getStylesheets().clear();

        String cssPath;
        switch (theme) {
            case "Dark Glass":
                cssPath = "/css/style-dark.css";
                break;
            case "Neumorphism":
                cssPath = "/css/style-neu.css";
                break;
            case "Light SaaS":
            default:
                cssPath = "/css/style-light.css";
                break;
        }

        rootPane.getStylesheets().add(getClass().getResource(cssPath).toExternalForm());
    }

    @FXML
    protected void handleLogin(ActionEvent event) {
        String user = txtUser.getText();
        String pass = txtPassword.getText();

        lblError.setVisible(false);
        lblError.setManaged(false);

        // Validación simple de prueba
        if ("admin".equals(user) && "admin".equals(pass)) {
            handleSuccessfulLogin();
        } else {
            // Animación Shake de Error
            showError("Usuario o contraseña incorrectos");
        }
    }

    private void showError(String msg) {
        lblError.setText(msg);
        lblError.setVisible(true);
        lblError.setManaged(true);
        shakeAnimation(loginCard);
    }

    private void shakeAnimation(javafx.scene.Node node) {
        TranslateTransition tt = new TranslateTransition(Duration.millis(50), node);
        tt.setFromX(0f);
        tt.setByX(10f);
        tt.setCycleCount(6);
        tt.setAutoReverse(true);
        tt.playFromStart();
    }

    private void handleSuccessfulLogin() {
        // Guardado de Preferencia si está marcado
        if (chkRememberMode.isSelected()) {
            prefs.putBoolean(PREF_REMEMBER, true);
            prefs.put(PREF_USER, txtUser.getText());
        } else {
            prefs.putBoolean(PREF_REMEMBER, false);
            prefs.remove(PREF_USER);
        }

        // Animación FadeOut de salida
        FadeTransition ftOut = new FadeTransition(Duration.millis(300), rootPane);
        ftOut.setFromValue(1.0);
        ftOut.setToValue(0.0);
        ftOut.setOnFinished(e -> loadDashboard());
        ftOut.play();
    }

    private void loadDashboard() {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/DashboardView.fxml"));
            Parent dashboardRoot = loader.load();

            // Pasa el tema actual a la nueva vista del dashboard para continuidad
            dashboardRoot.getStylesheets().addAll(rootPane.getStylesheets());

            Scene scene = rootPane.getScene();
            scene.setRoot(dashboardRoot);

            // Animación FadeIn de entrada
            FadeTransition ftIn = new FadeTransition(Duration.millis(400), dashboardRoot);
            ftIn.setFromValue(0.0);
            ftIn.setToValue(1.0);
            ftIn.play();

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

Descarga el Código Completo

Lleva tus habilidades a la práctica. Hemos empaquetado todo el proyecto junto con los tres archivos CSS completos y las vistas listas para ejecutar.


Ahora ya sabes cómo dar una experiencia profesional combinando FXML, transiciones suaves y temas CSS dinámicos directos en el corazón de JavaFX.

forumComentarios

Deja tu comentario

progress_activityCargando comentarios...