Tutorial: aplicación para calcular el cuadrado de un número
Este tutorial explica cómo se ha construido la aplicación incluida en c010-06-cuadrado. El documento parte del código original y avanza desde la estructura HTML hasta el comportamiento JavaScript, pasando por la presentación CSS, la accesibilidad, la usabilidad y la seguridad.
Importante: los tres archivos del apartado «Código completo original» se reproducen sin modificaciones. Cuando se comenta una posible simplificación, se identifica expresamente como opcional y se diferencia del código original.
Índice
- Presentación de la aplicación
- Código completo original
- Organización del proyecto
- A. Estructura y semántica HTML
- B. Presentación y organización del CSS
- C. Funcionamiento de JavaScript
- D. Accesibilidad
- E. Usabilidad
- F. Seguridad del código
- Revisión crítica del código
- Resumen y cierre
- Actividad de comprobación
- Lista de verificación
1. Presentación de la aplicación
La aplicación solicita un número y calcula su cuadrado, es decir, el resultado de multiplicarlo por sí mismo. Por ejemplo, el cuadrado de 5 es 25, porque 5 × 5 = 25. También admite números negativos y decimales.
Su objetivo didáctico es mostrar cómo colaboran las tres tecnologías básicas del desarrollo web:
- HTML define el significado y la estructura del contenido: el título, el formulario, el campo numérico, el botón y el resultado.
- CSS controla la presentación: colores, tamaños, separación, distribución, estados interactivos, adaptación a pantallas pequeñas y animaciones.
- JavaScript aporta el comportamiento: obtiene el número, lo valida, calcula su cuadrado y actualiza el resultado.
Los archivos se relacionan de la siguiente manera:
- El navegador abre
index.html. - El elemento
<link>hace que el navegador carguestyle.css. - El elemento
<script>cargascript.js. - JavaScript localiza mediante sus identificadores los elementos creados por HTML.
- Cuando se envía el formulario, JavaScript realiza el cálculo y modifica el elemento
<output>.
Al reproducir la aplicación, el alumnado aprenderá a:
- crear un documento HTML semántico;
- construir un formulario asociado correctamente a sus etiquetas y ayudas;
- organizar estilos mediante variables y la metodología BEM;
- diseñar una interfaz adaptable y visible al usar el teclado;
- escuchar el evento
submitconaddEventListener(); - obtener y validar un número;
- usar el operador de exponenciación
**; - mostrar datos como texto sin interpretarlos como HTML;
- distinguir entre accesibilidad, usabilidad y seguridad.
Alcance de la aplicación: es una aplicación íntegramente local. No envía información a un servidor, no guarda datos y no utiliza bibliotecas externas.
Vista previa funcional de la aplicación
Introduce un número y activa el botón para probar la misma aplicación que se analiza a continuación. También puedes abrirla de forma independiente en una pestaña nueva.
2. Código completo original
Antes de estudiar cada parte, conviene observar el código completo. Así se obtiene una visión general de los tres archivos que después se explicarán por bloques.
2.1. Archivo HTML: index.html
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Aplicación didáctica para calcular el cuadrado de un número." />
<title>Calcular el cuadrado de un número</title>
<link rel="stylesheet" href="./assets/style/style.css" />
<!--
defer hace que el script se ejecute cuando el HTML ya se ha
analizado, sin bloquear la construcción inicial de la página.
-->
<script src="./assets/js/script.js" defer></script>
</head>
<body class="page">
<!--
<main> identifica el contenido principal y único de la página.
aria-labelledby relaciona este contenido con su encabezado <h1>.
-->
<main class="app" aria-labelledby="titulo-aplicacion">
<header class="app__header">
<h1 id="titulo-aplicacion" class="app__title">Calcular el cuadrado</h1>
</header>
<div class="app__content">
<!--
El formulario se procesa con JavaScript. Su evento submit
permite activarlo tanto con el botón como con la tecla
Enter mientras el campo tiene el foco.
-->
<form id="formulario-cuadrado" class="calculation-form">
<div class="calculation-form__field">
<!--
for enlaza la etiqueta con el campo numero.
Al pulsar la etiqueta, el campo recibe el foco.
-->
<label class="calculation-form__label" for="numero">Introduce un número</label>
<!--
Este texto contiene instrucciones complementarias.
aria-describedby lo asociará al campo numérico.
-->
<p id="ayuda-numero" class="calculation-form__help">
Puedes utilizar números positivos, negativos o decimales.
</p>
<!--
type="number" solicita un dato numérico y step="any"
admite tanto enteros como números decimales.
required impide enviar el formulario vacío mediante
la validación nativa del navegador.
aria-describedby permite que los lectores de
pantalla anuncien la ayuda al enfocar el campo.
-->
<input
class="calculation-form__input"
type="number"
id="numero"
name="numero"
step="any"
inputmode="decimal"
aria-describedby="ayuda-numero"
required
/>
</div>
<!--
type="submit" conserva el comportamiento semántico del
formulario y permite utilizarlo con teclado.
-->
<button class="calculation-form__button" type="submit">Calcular el cuadrado</button>
</form>
<!--
La sección tiene un encabezado propio. aria-labelledby usa
ese encabezado como nombre accesible de la región.
-->
<section class="result" aria-labelledby="titulo-resultado">
<h2 id="titulo-resultado" class="result__title">Resultado del cálculo</h2>
<!--
<output> representa el resultado de una operación.
for lo relaciona con el dato de entrada y form indica a
qué formulario pertenece, aunque esté fuera de él.
role="status" y aria-live="polite" permiten anunciar
los cambios sin interrumpir bruscamente a quien use un
lector de pantalla. aria-atomic solicita leer el nuevo
resultado completo.
-->
<output
id="resultado"
class="result__output"
name="cuadrado"
for="numero"
form="formulario-cuadrado"
role="status"
aria-live="polite"
aria-atomic="true"
>El resultado aparecerá aquí.</output>
</section>
</div>
</main>
</body>
</html>
2.2. Archivo CSS: assets/style/style.css
/* + VARIABLES DEL TEMA */
:root {
--color-primary: #ffe000;
--color-primary-soft: #fff8bf;
--color-text: #191919;
--color-text-muted: #5f5f5f;
--color-surface: #ffffff;
--color-border: #d8d8d8;
--color-background: #f5f5f2;
--shadow-card: 0 1.25rem 3.5rem rgb(31 31 20 / 14%);
--radius-large: 1.5rem;
--radius-medium: 0.9rem;
}
/* + AJUSTES GENERALES */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
min-height: 100%;
color-scheme: light;
}
body {
margin: 0;
}
button,
input,
output {
font: inherit;
}
/* + BLOQUE BEM: PAGE */
.page {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(1rem, 5vw, 3rem);
background:
radial-gradient(circle at 10% 10%, rgb(255 224 0 / 28%), transparent 34rem),
linear-gradient(145deg, var(--color-background), #ecece7);
color: var(--color-text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
/* + BLOQUE BEM: APP */
.app {
position: relative;
width: min(100%, 48rem);
overflow: hidden;
border: 1px solid rgb(25 25 25 / 10%);
border-radius: var(--radius-large);
background-color: rgb(255 255 255 / 92%);
box-shadow: var(--shadow-card);
animation: app-entry 600ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
.app::before {
position: absolute;
inset: 0 0 auto;
height: 0.4rem;
background-color: var(--color-primary);
content: "";
}
.app__header {
padding: clamp(2rem, 6vw, 3.25rem) clamp(1.25rem, 5vw, 3rem) 1rem;
text-align: center;
}
.app__title {
margin: 0;
font-size: clamp(2rem, 8vw, 3.25rem);
line-height: 1.05;
letter-spacing: -0.045em;
}
.app__title::after {
display: block;
width: 3.75rem;
height: 0.35rem;
margin: 1rem auto 0;
border-radius: 999px;
background-color: var(--color-primary);
content: "";
}
.app__content {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1rem;
padding: 1rem clamp(1.25rem, 5vw, 3rem) clamp(1.5rem, 5vw, 3rem);
}
/* + BLOQUE BEM: CALCULATION-FORM */
.calculation-form {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1.5rem;
padding: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-medium);
background-color: var(--color-surface);
animation: content-entry 500ms 120ms ease-out both;
}
.calculation-form__field {
display: grid;
gap: 0.6rem;
}
.calculation-form__label {
font-size: 1.05rem;
font-weight: 750;
}
.calculation-form__help {
margin: 0;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.calculation-form__input {
width: 100%;
min-height: 3.25rem;
padding: 0.75rem 0.9rem;
border: 2px solid var(--color-border);
border-radius: 0.7rem;
background-color: var(--color-surface);
color: var(--color-text);
transition:
border-color 180ms ease,
box-shadow 180ms ease;
}
.calculation-form__input:hover {
border-color: #a7a7a7;
}
.calculation-form__input:focus-visible {
border-color: var(--color-text);
outline: none;
box-shadow: 0 0 0 0.25rem rgb(255 224 0 / 55%);
}
.calculation-form__button {
min-height: 3.25rem;
padding: 0.75rem 1rem;
border: 2px solid var(--color-text);
border-radius: 0.7rem;
background-color: var(--color-primary);
color: var(--color-text);
font-weight: 800;
cursor: pointer;
box-shadow: 0 0.35rem 0 var(--color-text);
transition:
transform 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
.calculation-form__button:hover {
background-color: #ffea3d;
transform: translateY(-0.12rem);
box-shadow: 0 0.47rem 0 var(--color-text);
}
.calculation-form__button:active {
transform: translateY(0.18rem);
box-shadow: 0 0.12rem 0 var(--color-text);
}
.calculation-form__button:focus-visible {
outline: 0.2rem solid var(--color-text);
outline-offset: 0.22rem;
}
/* + BLOQUE BEM: RESULT */
.result {
display: flex;
min-width: 0;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
border: 1px solid rgb(210 184 0 / 45%);
border-radius: var(--radius-medium);
background: linear-gradient(145deg, var(--color-primary-soft), #fffdf0);
animation: content-entry 500ms 200ms ease-out both;
}
.result__title {
margin: 0;
font-size: 1.15rem;
}
.result__output {
display: block;
min-height: 8rem;
padding: 1rem;
overflow-wrap: anywhere;
border: 2px dashed rgb(25 25 25 / 32%);
border-radius: 0.7rem;
background-color: rgb(255 255 255 / 78%);
color: var(--color-text);
font-size: 1.1rem;
white-space: pre-wrap;
}
/* + ANIMACIONES */
@keyframes app-entry {
from {
opacity: 0;
transform: translateY(0.8rem) scale(0.985);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes content-entry {
from {
opacity: 0;
transform: translateY(0.45rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* + DISEÑO ADAPTABLE */
@media (max-width: 42rem) {
.app__content {
grid-template-columns: 1fr;
}
.result__output {
min-height: 6rem;
}
}
/* Respeta la preferencia de quienes necesitan reducir el movimiento. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
2.3. Archivo JavaScript: assets/js/script.js
'use strict';
/*
* Referencias a los tres elementos que intervienen en la aplicación.
* Se obtienen una sola vez para evitar búsquedas repetidas en el DOM.
*/
const formularioCuadrado = document.getElementById('formulario-cuadrado');
const campoNumero = document.getElementById('numero');
const resultado = document.getElementById('resultado');
/*
* Esta comprobación facilita detectar un error en el HTML si se cambia o se
* elimina accidentalmente alguno de los identificadores anteriores.
*/
if (!formularioCuadrado || !campoNumero || !resultado) {
throw new Error('No se han encontrado todos los elementos necesarios de la aplicación.');
}
/*
* Esta es la función manejadora del evento submit. Se escribe con un parámetro
* porque el navegador le entrega automáticamente el objeto del evento.
*/
function calcularCuadrado(evento) {
/* Evita que el formulario recargue la página al enviarse. */
evento.preventDefault();
/* valueAsNumber obtiene directamente el valor numérico del campo. */
const numero = campoNumero.valueAsNumber;
/*
* La validación nativa impide normalmente llegar aquí con un dato vacío o
* incorrecto. Esta comprobación adicional hace que la función siga siendo
* segura si se invoca desde otro código.
*/
if (!Number.isFinite(numero)) {
resultado.value = 'Introduce un número válido.';
campoNumero.focus();
return;
}
const cuadrado = numero ** 2;
/* Evita mostrar Infinity cuando el resultado excede el rango de JavaScript. */
if (!Number.isFinite(cuadrado)) {
resultado.value = 'El número es demasiado grande para calcular su cuadrado.';
return;
}
const base = numero < 0 ? `(${numero})` : String(numero);
/*
* value actualiza el elemento <output> como texto. No se usa innerHTML, de
* modo que el contenido introducido no puede interpretarse como código HTML.
*/
resultado.value = `${base}² = ${cuadrado}`;
}
/*
* Se pasa calcularCuadrado sin paréntesis porque se entrega una referencia a la
* función. El navegador la ejecutará únicamente cuando se envíe el formulario.
*
* calcularCuadrado -> referencia a la función
* calcularCuadrado() -> ejecución inmediata de la función
*/
formularioCuadrado.addEventListener('submit', calcularCuadrado);
3. Organización del proyecto
Para que las rutas relativas del HTML funcionen, hay que conservar esta organización:
c010-06-cuadrado/index.htmlassets/style/style.css
js/script.js
El punto . con el que empiezan las rutas significa «desde la carpeta actual». Por tanto, desde index.html, esta ruta busca el CSS dentro de assets/style/:
<link rel="stylesheet" href="./assets/style/style.css" />
Y esta otra busca el JavaScript dentro de assets/js/:
<script src="./assets/js/script.js" defer></script>
Error habitual: cambiar el nombre de una carpeta o trasladar un archivo sin actualizar su ruta. En ese caso, la página puede aparecer sin estilos o no reaccionar al botón.
4. A. Estructura y semántica HTML
La semántica HTML consiste en elegir elementos que describan el significado del contenido, no solo su aspecto. Por ejemplo, <button> expresa una acción, <form> agrupa controles relacionados y <output> representa el resultado de un cálculo. Esta elección ayuda al navegador, a los lectores de pantalla, a los buscadores y a quienes mantengan el código.
Paso 1. Crear el documento y sus metadatos
1. Objetivo del paso
Preparar un documento HTML válido, declarar su idioma, configurar la codificación y la visualización en dispositivos móviles, y conectar los otros dos archivos.
2. Explicación teórica
La cabecera <head> contiene información sobre la página que no forma parte de su contenido principal. La declaración <!doctype html> activa el modo estándar de HTML moderno. Los metadatos indican al navegador cómo interpretar y presentar el documento.
3. Fragmento exacto relacionado
<!doctype html>
<html lang="es">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Aplicación didáctica para calcular el cuadrado de un número." />
<title>Calcular el cuadrado de un número</title>
<link rel="stylesheet" href="./assets/style/style.css" />
<!--
defer hace que el script se ejecute cuando el HTML ya se ha
analizado, sin bloquear la construcción inicial de la página.
-->
<script src="./assets/js/script.js" defer></script>
</head>
4. Cómo funciona
lang="es"declara que el contenido está en español.charset="UTF-8"permite representar correctamente letras acentuadas, lañy el símbolo².viewportadapta el ancho de la página al dispositivo.descriptionresume el propósito del documento.<title>proporciona el nombre que aparece en la pestaña del navegador.<link>carga el CSS.<script>carga JavaScript ydeferretrasa su ejecución hasta que el HTML ya se ha analizado.
5. Relación con el resto de la aplicación
Sin <link>, el contenido conservaría su estructura, pero perdería la presentación diseñada. Sin <script>, el formulario seguiría apareciendo, pero no se realizaría el cálculo. Sin defer, este script concreto podría intentar buscar elementos antes de que el navegador los hubiera creado si se ejecutase demasiado pronto.
6. Errores habituales
- escribir una ruta que no coincide con las carpetas reales;
- omitir
lang, lo que dificulta que las tecnologías de asistencia elijan la pronunciación correcta; - colocar un valor de codificación incorrecto;
- olvidar
defercuando el script está en<head>y depende de elementos posteriores del documento.
7. Conclusión del paso
El documento ya conoce su idioma, su codificación, su comportamiento en pantallas móviles y la ubicación de los archivos de presentación y comportamiento.
Paso 2. Definir el contenido principal y la jerarquía de encabezados
1. Objetivo del paso
Crear una zona principal claramente identificada y darle un único encabezado de nivel 1.
2. Explicación teórica
<main> representa el contenido principal de la página. <header> introduce el contenido que encabeza y <h1> expresa el tema general. La jerarquía no se elige por el tamaño visual, sino por la organización lógica del contenido.
3. Fragmento exacto relacionado
<body class="page">
<!--
<main> identifica el contenido principal y único de la página.
aria-labelledby relaciona este contenido con su encabezado <h1>.
-->
<main class="app" aria-labelledby="titulo-aplicacion">
<header class="app__header">
<h1 id="titulo-aplicacion" class="app__title">Calcular el cuadrado</h1>
</header>
4. Cómo funciona
La clase page permite aplicar estilos al cuerpo completo. La clase app identifica visualmente el bloque principal. aria-labelledby="titulo-aplicacion" toma como nombre del bloque el texto del elemento cuyo id es titulo-aplicacion. El <h1> comunica el propósito de toda la aplicación.
5. Relación con el resto de la aplicación
Dentro de <main> se incluirán el formulario y el resultado. Más adelante, el resultado tendrá un <h2> porque es una subsección del tema presentado por el <h1>.
6. Errores habituales
- usar un
<div>para todo sin aprovechar los elementos semánticos; - elegir
<h1>o<h2>por su tamaño predeterminado; - repetir un mismo
id; - escribir en
aria-labelledbyun identificador que no existe.
7. Conclusión del paso
La página dispone de un contenido principal y de una jerarquía de encabezados comprensible tanto visualmente como mediante tecnologías de asistencia.
Paso 3. Construir el formulario accesible
1. Objetivo del paso
Solicitar un número mediante un control apropiado, explicar qué valores admite y proporcionar un botón de envío.
2. Explicación teórica
Un formulario agrupa datos y acciones relacionadas. Cada control debe tener un nombre visible mediante <label>. Las instrucciones adicionales pueden enlazarse con aria-describedby. El botón con type="submit" envía el formulario y permite centralizar el comportamiento en el evento submit.
3. Fragmento exacto relacionado
<form id="formulario-cuadrado" class="calculation-form">
<div class="calculation-form__field">
<!--
for enlaza la etiqueta con el campo numero.
Al pulsar la etiqueta, el campo recibe el foco.
-->
<label class="calculation-form__label" for="numero">Introduce un número</label>
<!--
Este texto contiene instrucciones complementarias.
aria-describedby lo asociará al campo numérico.
-->
<p id="ayuda-numero" class="calculation-form__help">
Puedes utilizar números positivos, negativos o decimales.
</p>
<!--
type="number" solicita un dato numérico y step="any"
admite tanto enteros como números decimales.
required impide enviar el formulario vacío mediante
la validación nativa del navegador.
aria-describedby permite que los lectores de
pantalla anuncien la ayuda al enfocar el campo.
-->
<input
class="calculation-form__input"
type="number"
id="numero"
name="numero"
step="any"
inputmode="decimal"
aria-describedby="ayuda-numero"
required
/>
</div>
<!--
type="submit" conserva el comportamiento semántico del
formulario y permite utilizarlo con teclado.
-->
<button class="calculation-form__button" type="submit">Calcular el cuadrado</button>
</form>
4. Cómo funciona
- El
iddel formulario permitirá encontrarlo desde JavaScript y asociarle el<output>. for="numero"en<label>coincide conid="numero"en<input>.type="number"comunica que se espera un dato numérico.step="any"elimina la restricción predeterminada a determinados incrementos y permite decimales.inputmode="decimal"sugiere un teclado decimal en dispositivos compatibles.requiredactiva la validación nativa cuando el campo está vacío.aria-describedby="ayuda-numero"añade al campo la descripción del párrafo de ayuda.- El botón de tipo
submitpermite enviar el formulario al hacer clic o al pulsarEnterdesde el campo.
5. Relación con el resto de la aplicación
JavaScript localizará el formulario por formulario-cuadrado y el campo por numero. El valor del campo será el dato de entrada del cálculo. El envío no se gestiona con un onclick aislado, sino mediante el evento del formulario completo.
6. Errores habituales
- dejar vacío el atributo
forde la etiqueta; - hacer que
foreidtengan valores diferentes; - usar
type="text"sin necesidad y tener que resolver más validación manual; - omitir
step="any"y provocar que algunos decimales no superen la validación; - usar un
<div>con aspecto de botón en lugar de un<button>real; - escuchar solo el clic y olvidar el envío mediante teclado.
7. Conclusión del paso
El formulario expresa con claridad qué dato se solicita, qué valores admite y cuál es la acción principal, además de conservar los comportamientos nativos del navegador.
Paso 4. Crear una zona semántica para el resultado
1. Objetivo del paso
Reservar una sección identificable en la que JavaScript mostrará el resultado del cálculo.
2. Explicación teórica
Una <section> agrupa contenido temáticamente relacionado y normalmente posee un encabezado. <output> es el elemento específico para representar el resultado de una operación realizada a partir de datos de formulario.
3. Fragmento exacto relacionado
<section class="result" aria-labelledby="titulo-resultado">
<h2 id="titulo-resultado" class="result__title">Resultado del cálculo</h2>
<!--
<output> representa el resultado de una operación.
for lo relaciona con el dato de entrada y form indica a
qué formulario pertenece, aunque esté fuera de él.
role="status" y aria-live="polite" permiten anunciar
los cambios sin interrumpir bruscamente a quien use un
lector de pantalla. aria-atomic solicita leer el nuevo
resultado completo.
-->
<output
id="resultado"
class="result__output"
name="cuadrado"
for="numero"
form="formulario-cuadrado"
role="status"
aria-live="polite"
aria-atomic="true"
>El resultado aparecerá aquí.</output>
</section>
4. Cómo funciona
El <h2> nombra la sección. for="numero" relaciona el resultado con el campo empleado en el cálculo y form="formulario-cuadrado" lo asocia al formulario aunque el <output> esté fuera de él. La región activa anuncia los cambios de manera cortés y aria-atomic="true" solicita leer el mensaje actualizado completo.
5. Relación con el resto de la aplicación
El id="resultado" permite que JavaScript obtenga una referencia al elemento y cambie su propiedad value. El texto inicial informa de que todavía no se ha realizado ningún cálculo.
6. Errores habituales
- mostrar el resultado en un elemento sin significado cuando existe
<output>; - repetir el
ido cambiarlo solo en HTML, dejando JavaScript desactualizado; - omitir el encabezado de la sección;
- actualizar la interfaz con
innerHTMLaunque únicamente sea necesario mostrar texto.
7. Conclusión del paso
El resultado queda situado en una región semántica, relacionada con su dato de entrada y preparada para recibir actualizaciones dinámicas.
5. B. Presentación y organización del CSS
CSS aplica reglas formadas por un selector y un conjunto de declaraciones. El selector decide qué elementos se ven afectados; las propiedades definen su presentación. El archivo utiliza variables, selectores de clase, pseudoclases, pseudoelementos, reglas @media y animaciones.
También emplea BEM (Block, Element, Modifier), una convención de nombres que distingue bloques independientes y elementos pertenecientes a esos bloques. Por ejemplo:
| Nombre | Tipo BEM | Significado |
|---|---|---|
.app |
Bloque | Componente general de la aplicación. |
.app__title |
Elemento | Título que pertenece al bloque app. |
.calculation-form |
Bloque | Formulario de cálculo. |
.calculation-form__input |
Elemento | Campo perteneciente al formulario. |
.result__output |
Elemento | Salida perteneciente al bloque result. |
No aparecen modificadores del tipo .bloque--variante, porque la aplicación no necesita variantes de sus componentes.
Paso 5. Definir variables y ajustes generales
1. Objetivo del paso
Centralizar los valores visuales repetidos y establecer una base coherente para todos los elementos.
2. Explicación teórica
Las propiedades personalizadas de CSS, conocidas habitualmente como variables CSS, se declaran con nombres que empiezan por -- y se recuperan mediante var(). :root representa el elemento raíz y permite reutilizarlas en todo el documento.
3. Fragmento exacto relacionado
/* + VARIABLES DEL TEMA */
:root {
--color-primary: #ffe000;
--color-primary-soft: #fff8bf;
--color-text: #191919;
--color-text-muted: #5f5f5f;
--color-surface: #ffffff;
--color-border: #d8d8d8;
--color-background: #f5f5f2;
--shadow-card: 0 1.25rem 3.5rem rgb(31 31 20 / 14%);
--radius-large: 1.5rem;
--radius-medium: 0.9rem;
}
/* + AJUSTES GENERALES */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
min-height: 100%;
color-scheme: light;
}
body {
margin: 0;
}
button,
input,
output {
font: inherit;
}
4. Cómo funciona
Las variables reúnen colores, sombras y radios de borde. box-sizing: border-box hace que el ancho y el alto declarados incluyan el relleno y el borde. Se elimina el margen predeterminado de <body>. Los controles heredan la tipografía para mantener la coherencia visual. color-scheme: light indica que la interfaz se ha diseñado con un esquema claro.
5. Relación con el resto de la aplicación
Las reglas posteriores reutilizan valores como var(--color-primary) o var(--radius-medium). Cambiar una variable permite actualizar varias partes de la interfaz desde un único lugar.
6. Errores habituales
- escribir
var(--color-primary)con un nombre distinto del declarado; - creer que
box-sizingse hereda automáticamente y aplicarlo solo ahtml; - eliminar todos los estilos nativos sin reconstruir estados importantes como el foco;
- repetir colores muchas veces en lugar de emplear las variables existentes.
7. Conclusión del paso
La hoja de estilos dispone de una base predecible y de un tema fácil de mantener.
Paso 6. Centrar y presentar el bloque principal
1. Objetivo del paso
Crear el fondo de página, centrar la aplicación y presentarla como una tarjeta de anchura limitada.
2. Explicación teórica
CSS Grid puede centrar un único elemento con place-items: center. Funciones como min() y clamp() permiten que las medidas se adapten al espacio disponible sin crecer o reducirse más allá de ciertos límites.
3. Fragmento exacto relacionado
.page {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(1rem, 5vw, 3rem);
background:
radial-gradient(circle at 10% 10%, rgb(255 224 0 / 28%), transparent 34rem),
linear-gradient(145deg, var(--color-background), #ecece7);
color: var(--color-text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.5;
}
.app {
position: relative;
width: min(100%, 48rem);
overflow: hidden;
border: 1px solid rgb(25 25 25 / 10%);
border-radius: var(--radius-large);
background-color: rgb(255 255 255 / 92%);
box-shadow: var(--shadow-card);
animation: app-entry 600ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
4. Cómo funciona
.page ocupa al menos toda la altura visible y centra .app. El relleno evita que la tarjeta toque los bordes. width: min(100%, 48rem) hace que la tarjeta use el ancho disponible sin superar 48rem. Los degradados crean profundidad sin imágenes externas. La pila tipográfica recurre a fuentes del sistema si Inter no está instalada.
5. Relación con el resto de la aplicación
.page corresponde a <body class="page"> y .app a <main class="app">. El resto de componentes se distribuye dentro de esta tarjeta.
6. Errores habituales
- declarar
height: 100vhy provocar desbordamiento cuando el contenido necesita más altura; - olvidar el límite máximo de anchura y obtener líneas demasiado extensas en pantallas grandes;
- aplicar
place-itemssin establecer antes un contenedor Grid; - usar una fuente externa no cargada y confiar en que siempre estará disponible.
7. Conclusión del paso
La aplicación queda centrada, mantiene márgenes seguros y se adapta tanto a ventanas estrechas como amplias.
Paso 7. Distribuir y dimensionar el formulario y el resultado
1. Objetivo del paso
Organizar el formulario y el resultado en dos columnas equilibradas, además de darles una apariencia diferenciada.
2. Explicación teórica
Grid es apropiado para distribuir bloques en filas y columnas. Flexbox resulta útil para ordenar el contenido interno en una sola dirección. La aplicación combina ambos sistemas según la necesidad.
3. Fragmento exacto relacionado
.app__content {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 1rem;
padding: 1rem clamp(1.25rem, 5vw, 3rem) clamp(1.5rem, 5vw, 3rem);
}
.calculation-form {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1.5rem;
padding: 1.5rem;
border: 1px solid var(--color-border);
border-radius: var(--radius-medium);
background-color: var(--color-surface);
animation: content-entry 500ms 120ms ease-out both;
}
.result {
display: flex;
min-width: 0;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
border: 1px solid rgb(210 184 0 / 45%);
border-radius: var(--radius-medium);
background: linear-gradient(145deg, var(--color-primary-soft), #fffdf0);
animation: content-entry 500ms 200ms ease-out both;
}
4. Cómo funciona
grid-template-columns crea dos columnas del mismo tamaño. minmax(0, 1fr) permite que el contenido pueda reducirse sin forzar un desbordamiento. El formulario y el resultado son contenedores flexibles verticales. Sus fondos y bordes diferentes permiten reconocer la zona de entrada y la zona de salida.
5. Relación con el resto de la aplicación
Las clases coinciden con <div class="app__content">, <form class="calculation-form"> y <section class="result">. La distribución visual refuerza la secuencia conceptual «dato de entrada → resultado».
6. Errores habituales
- usar anchos fijos para las columnas;
- olvidar
gapy separar los componentes mediante márgenes difíciles de mantener; - confundir
justify-contentconalign-items; - omitir
min-width: 0cuando el contenido de un elemento Grid o Flex puede crecer demasiado.
7. Conclusión del paso
La interfaz queda dividida en dos áreas claras y equilibradas, cada una organizada con el sistema de disposición más apropiado.
Paso 8. Diseñar los controles y sus estados interactivos
1. Objetivo del paso
Hacer que el campo y el botón sean legibles, cómodos de pulsar y reconocibles cuando reciben el puntero, el foco o una pulsación.
2. Explicación teórica
Las pseudoclases describen estados. :hover se activa al situar el puntero, :active durante la activación y :focus-visible cuando el navegador considera necesario mostrar una indicación de foco, especialmente al navegar con teclado.
3. Fragmento exacto relacionado
.calculation-form__input {
width: 100%;
min-height: 3.25rem;
padding: 0.75rem 0.9rem;
border: 2px solid var(--color-border);
border-radius: 0.7rem;
background-color: var(--color-surface);
color: var(--color-text);
transition:
border-color 180ms ease,
box-shadow 180ms ease;
}
.calculation-form__input:hover {
border-color: #a7a7a7;
}
.calculation-form__input:focus-visible {
border-color: var(--color-text);
outline: none;
box-shadow: 0 0 0 0.25rem rgb(255 224 0 / 55%);
}
.calculation-form__button {
min-height: 3.25rem;
padding: 0.75rem 1rem;
border: 2px solid var(--color-text);
border-radius: 0.7rem;
background-color: var(--color-primary);
color: var(--color-text);
font-weight: 800;
cursor: pointer;
box-shadow: 0 0.35rem 0 var(--color-text);
transition:
transform 160ms ease,
box-shadow 160ms ease,
background-color 160ms ease;
}
4. Cómo funciona
La altura mínima y el relleno crean objetivos de interacción amplios. El campo cambia de borde al pasar el puntero y recibe un halo amarillo al enfocarse. El botón utiliza el color principal y una sombra que sugiere volumen. Las transiciones suavizan únicamente las propiedades que cambian.
5. Relación con el resto de la aplicación
Los estados visuales acompañan el comportamiento nativo de <input> y <button>. No sustituyen la semántica: la interacción por teclado procede de los elementos HTML, mientras CSS hace visible su estado.
6. Errores habituales
- eliminar
outlinesin crear una alternativa de foco claramente visible; - aplicar la transición a
all, lo que puede animar propiedades no previstas; - mostrar cambios solo en
:hovery olvidar a quienes navegan con teclado; - usar colores de texto y fondo con poco contraste.
7. Conclusión del paso
Los controles comunican visualmente cuándo están disponibles, enfocados o activados, y conservan una superficie de interacción cómoda.
Paso 9. Añadir animaciones, adaptación y reducción de movimiento
1. Objetivo del paso
Introducir movimientos sutiles, reorganizar el contenido en pantallas pequeñas y respetar a las personas que prefieren menos animación.
2. Explicación teórica
@keyframes define etapas de una animación. Una consulta @media aplica reglas solo cuando se cumple una condición. prefers-reduced-motion consulta una preferencia configurada por el usuario en su sistema operativo o navegador.
3. Fragmento exacto relacionado
@keyframes content-entry {
from {
opacity: 0;
transform: translateY(0.45rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* + DISEÑO ADAPTABLE */
@media (max-width: 42rem) {
.app__content {
grid-template-columns: 1fr;
}
.result__output {
min-height: 6rem;
}
}
/* Respeta la preferencia de quienes necesitan reducir el movimiento. */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
4. Cómo funciona
content-entry hace aparecer el contenido con un pequeño desplazamiento vertical. Cuando la ventana no supera 42rem, la cuadrícula cambia de dos columnas a una. Si el usuario solicita movimiento reducido, las duraciones pasan prácticamente a cero y las animaciones solo se ejecutan una vez.
5. Relación con el resto de la aplicación
El formulario y el resultado usan content-entry con retrasos distintos. La consulta adaptable modifica .app__content, creada en el paso anterior, sin alterar el orden del HTML: primero continúa apareciendo el formulario y después el resultado.
6. Errores habituales
- crear una animación y no aplicarla a ningún selector;
- usar movimientos continuos o intensos para una acción sencilla;
- resolver el diseño móvil cambiando visualmente el orden lógico del contenido;
- olvidar
prefers-reduced-motion.
7. Conclusión del paso
La presentación es agradable sin depender del movimiento y conserva una distribución legible en pantallas pequeñas.
6. C. Funcionamiento de JavaScript
JavaScript trabaja con el DOM (Document Object Model), la representación en memoria que el navegador crea a partir del HTML. A través del DOM, el programa puede encontrar elementos, escuchar acciones y actualizar contenido.
El flujo completo es el siguiente:
- El navegador analiza el HTML.
- Gracias a
defer, ejecuta el script cuando los elementos ya existen. - El script guarda referencias al formulario, al campo y al resultado.
- Registra una función para el evento
submit. - El usuario introduce un número y envía el formulario.
- La función evita la recarga, obtiene y valida el dato.
- Calcula el cuadrado.
- Actualiza
<output>con un mensaje textual.
Paso 10. Activar el modo estricto y seleccionar elementos
1. Objetivo del paso
Preparar un entorno de ejecución más riguroso y guardar referencias a los elementos que JavaScript necesita.
2. Explicación teórica
'use strict' activa el modo estricto, que convierte algunos errores silenciosos en errores visibles y evita determinados comportamientos antiguos. document.getElementById() busca el elemento cuyo id coincide exactamente con el texto indicado.
3. Fragmento exacto relacionado
'use strict';
/*
* Referencias a los tres elementos que intervienen en la aplicación.
* Se obtienen una sola vez para evitar búsquedas repetidas en el DOM.
*/
const formularioCuadrado = document.getElementById('formulario-cuadrado');
const campoNumero = document.getElementById('numero');
const resultado = document.getElementById('resultado');
4. Cómo funciona
Cada llamada devuelve el elemento encontrado o null si no existe. Las referencias se guardan con const porque las variables no se reasignarán a otros elementos. Esto no vuelve inmutable al elemento: JavaScript puede seguir cambiando su valor o contenido.
5. Relación con el resto de la aplicación
Los identificadores deben coincidir con los del HTML. formularioCuadrado servirá para registrar el evento, campoNumero proporcionará el dato y resultado recibirá el mensaje.
6. Errores habituales
- incluir
#engetElementById('#numero'); este método recibe el identificador sin almohadilla; - cambiar un
iden HTML y no actualizar JavaScript; - confundir
constcon la inmutabilidad total del objeto referenciado; - consultar repetidamente el DOM dentro de la función sin necesidad.
7. Conclusión del paso
El programa dispone de referencias estables a los tres elementos que intervienen en el flujo.
Paso 11. Comprobar que el HTML y JavaScript siguen coordinados
1. Objetivo del paso
Detectar inmediatamente si falta alguno de los elementos esenciales.
2. Explicación teórica
Una comprobación defensiva verifica una condición que normalmente debería cumplirse, pero que podría dejar de hacerlo después de modificar el código. El operador ! niega un valor; || significa «o»; throw detiene la ejecución lanzando un error explícito.
3. Fragmento exacto relacionado
/*
* Esta comprobación facilita detectar un error en el HTML si se cambia o se
* elimina accidentalmente alguno de los identificadores anteriores.
*/
if (!formularioCuadrado || !campoNumero || !resultado) {
throw new Error('No se han encontrado todos los elementos necesarios de la aplicación.');
}
4. Cómo funciona
Si al menos una referencia vale null, la condición se cumple y se lanza el error. En lugar de fallar más adelante con un mensaje poco claro, la consola indica la causa general: falta un elemento necesario.
5. Relación con el resto de la aplicación
Solo cuando existen las tres referencias se continúa hasta el registro del evento. defer reduce la posibilidad de un fallo por ejecutar el script demasiado pronto; la comprobación también detecta identificadores eliminados o escritos de forma distinta.
6. Errores habituales
- usar
&&y detectar el problema únicamente cuando faltan los tres elementos a la vez; - mostrar el error al usuario como si fuera un dato incorrecto introducido por él;
- continuar la ejecución después de descubrir que falta un elemento esencial;
- incluir datos sensibles en mensajes de error del cliente.
7. Conclusión del paso
El código falla de forma temprana y comprensible si se rompe la relación entre los archivos.
Paso 12. Registrar y gestionar el envío del formulario
1. Objetivo del paso
Ejecutar el cálculo cuando se envíe el formulario y evitar que la página se recargue.
2. Explicación teórica
Un evento es un suceso detectado por el navegador. Una función manejadora es la función que se ejecuta como respuesta. addEventListener() asocia ambos sin escribir JavaScript dentro del HTML.
3. Fragmento exacto relacionado
function calcularCuadrado(evento) {
/* Evita que el formulario recargue la página al enviarse. */
evento.preventDefault();
/*
* Se pasa calcularCuadrado sin paréntesis porque se entrega una referencia a la
* función. El navegador la ejecutará únicamente cuando se envíe el formulario.
*
* calcularCuadrado -> referencia a la función
* calcularCuadrado() -> ejecución inmediata de la función
*/
formularioCuadrado.addEventListener('submit', calcularCuadrado);
4. Cómo funciona
El navegador entrega automáticamente a la función un objeto evento. preventDefault() cancela el comportamiento predeterminado de envío, que en esta aplicación recargaría la página. Al final del archivo se registra calcularCuadrado como manejadora de submit; se pasa sin paréntesis para que no se ejecute de inmediato.
5. Relación con el resto de la aplicación
El evento procede del <form> y no solo del botón. Por eso funciona tanto al hacer clic como al pulsar Enter desde el campo, siempre que la validación nativa permita el envío.
6. Errores habituales
- escribir
addEventListener('submit', calcularCuadrado())y ejecutar la función durante la carga; - usar
onclicken el botón y perder la idea de que se está enviando un formulario; - olvidar
preventDefault()y ver desaparecer el resultado tras la recarga; - escribir
evento.preventDefaultsin los paréntesis, lo que no ejecuta el método.
7. Conclusión del paso
La aplicación responde al envío semántico del formulario y mantiene la página disponible para presentar el resultado.
Paso 13. Obtener, validar y procesar el número
1. Objetivo del paso
Convertir el contenido del campo en un número utilizable, rechazar valores no finitos y calcular el cuadrado.
2. Explicación teórica
La propiedad valueAsNumber devuelve el valor numérico de un campo type="number". Si no puede producir un número válido, devuelve NaN. Number.isFinite() acepta únicamente números finitos: descarta NaN, Infinity y -Infinity. El operador ** realiza una exponenciación.
3. Fragmento exacto relacionado
/* valueAsNumber obtiene directamente el valor numérico del campo. */
const numero = campoNumero.valueAsNumber;
/*
* La validación nativa impide normalmente llegar aquí con un dato vacío o
* incorrecto. Esta comprobación adicional hace que la función siga siendo
* segura si se invoca desde otro código.
*/
if (!Number.isFinite(numero)) {
resultado.value = 'Introduce un número válido.';
campoNumero.focus();
return;
}
const cuadrado = numero ** 2;
/* Evita mostrar Infinity cuando el resultado excede el rango de JavaScript. */
if (!Number.isFinite(cuadrado)) {
resultado.value = 'El número es demasiado grande para calcular su cuadrado.';
return;
}
4. Cómo funciona
Primero se obtiene el número. Si no es finito, se escribe un mensaje, se devuelve el foco al campo y return termina la función. Si es correcto, se eleva a 2. Después se comprueba también el resultado, porque un número aceptado puede producir Infinity al elevarse si es extraordinariamente grande.
5. Relación con el resto de la aplicación
HTML aporta una primera validación mediante type="number" y required. JavaScript añade una comprobación defensiva y otra que HTML no puede realizar: verificar que el resultado siga dentro del rango numérico finito.
6. Errores habituales
- usar
campoNumero.valuey olvidar que devuelve texto; - comprobar solo
numero !== NaN, una comparación que nunca detecta correctamenteNaN; - confundir
numero ** 2connumero * 2; - olvidar
returny continuar el cálculo tras mostrar un error; - validar la entrada pero no el posible desbordamiento del resultado.
7. Conclusión del paso
El programa obtiene un número real de la interfaz, aplica dos controles coherentes y calcula su cuadrado solo cuando puede hacerlo de forma representable.
Paso 14. Formatear y mostrar el resultado de forma segura
1. Objetivo del paso
Crear un mensaje comprensible para números positivos y negativos y escribirlo como texto en <output>.
2. Explicación teórica
El operador condicional condición ? valor1 : valor2 elige entre dos valores. Las plantillas literales, delimitadas con acentos graves, permiten insertar expresiones mediante ${...}. La propiedad value de <output> cambia el resultado mostrado.
3. Fragmento exacto relacionado
const base = numero < 0 ? `(${numero})` : String(numero);
/*
* value actualiza el elemento <output> como texto. No se usa innerHTML, de
* modo que el contenido introducido no puede interpretarse como código HTML.
*/
resultado.value = `${base}² = ${cuadrado}`;
4. Cómo funciona
Si el número es negativo, se rodea de paréntesis: (-3)² = 9. Esto evita la ambigüedad visual de -3², que matemáticamente suele interpretarse como -(3²). Si no es negativo, String(numero) crea su representación textual. Finalmente se compone y asigna el mensaje.
5. Relación con el resto de la aplicación
resultado referencia el <output> del HTML. La región activa ya existe cuando se actualiza, por lo que las tecnologías de asistencia pueden detectar el cambio. CSS da al mensaje espacio, contraste y ajuste de línea.
6. Errores habituales
- omitir los paréntesis al presentar una base negativa;
- emplear comillas normales con
${base}y esperar interpolación; - usar
innerHTMLpara una salida que solo requiere texto; - modificar una variable distinta de la que referencia el
<output>.
7. Conclusión del paso
La aplicación presenta una expresión matemática clara y mantiene el dato como texto, sin convertirlo en marcado HTML.
7. D. Accesibilidad
La accesibilidad web busca que una página pueda ser percibida, comprendida y utilizada por el mayor número posible de personas, incluidas quienes emplean teclado, lectores de pantalla, ampliación, alto contraste o reducción de movimiento.
7.1. Medidas presentes y beneficio que aportan
| Medida presente | Fragmento relacionado | Beneficio principal |
|---|---|---|
| Idioma declarado | <html lang="es"> |
Facilita la pronunciación correcta del contenido. |
| Contenido principal | <main class="app" ...> |
Permite reconocer el contenido principal como un punto de referencia. |
| Encabezados ordenados | <h1 ...> y <h2 ...> |
Expresan la estructura de la página. |
| Etiqueta visible | <label ... for="numero"> |
Da nombre al campo y amplía su zona de activación. |
| Ayuda asociada | aria-describedby="ayuda-numero" |
Hace que la instrucción adicional se anuncie junto al campo. |
| Controles nativos | <input> y <button type="submit"> |
Conservan teclado, foco, activación y significado propios del navegador. |
| Salida semántica | <output ...> |
Identifica el contenido como resultado de una operación. |
| Actualización anunciable | role="status", aria-live="polite" y aria-atomic="true" |
Facilita el anuncio no intrusivo del mensaje actualizado. |
| Foco visible | .calculation-form__input:focus-visible y .calculation-form__button:focus-visible |
Permite saber qué control recibirá la siguiente acción del teclado. |
| Movimiento reducido | @media (prefers-reduced-motion: reduce) |
Evita imponer animaciones a quien ha pedido reducirlas. |
7.2. Asociación entre etiqueta, ayuda y campo
La etiqueta y el campo se conectan mediante el mismo identificador:
<label class="calculation-form__label" for="numero">Introduce un número</label>
<input
class="calculation-form__input"
type="number"
id="numero"
name="numero"
step="any"
inputmode="decimal"
aria-describedby="ayuda-numero"
required
/>
El texto de ayuda tiene el identificador citado por aria-describedby:
<p id="ayuda-numero" class="calculation-form__help">
Puedes utilizar números positivos, negativos o decimales.
</p>
No debe sustituirse la etiqueta por un placeholder. El texto de un placeholder puede desaparecer al escribir, suele tener menos contraste y no siempre funciona como nombre accesible fiable.
7.4. Mensaje de resultado
El resultado se actualiza sin trasladar forzosamente el foco:
resultado.value = `${base}² = ${cuadrado}`;
La región utiliza un anuncio cortés, que no debería interrumpir de forma brusca el contenido que ya estuviera leyendo una tecnología de asistencia. Cuando JavaScript detecta una entrada no finita por su comprobación defensiva, devuelve el foco al campo:
if (!Number.isFinite(numero)) {
resultado.value = 'Introduce un número válido.';
campoNumero.focus();
return;
}
En el uso normal, un campo vacío queda detenido antes por la validación nativa de required, y el navegador presenta su propio aviso.
7.5. Contraste, foco y movimiento
Los pares de colores principales presentan un contraste alto: #191919 sobre #ffe000 alcanza aproximadamente 13,32:1, y #5f5f5f sobre blanco aproximadamente 6,39:1. Esto favorece la lectura del botón y del texto de ayuda.
El código solo elimina el contorno nativo del campo después de crear una alternativa visible:
.calculation-form__input:focus-visible {
border-color: var(--color-text);
outline: none;
box-shadow: 0 0 0 0.25rem rgb(255 224 0 / 55%);
}
Advertencia: outline: none sería un problema si no existieran el borde oscuro y el halo de sustitución.
La reducción de movimiento es global y se aplica también a pseudoelementos:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
7.6. Imágenes y texto alternativo
La aplicación no contiene imágenes. Por tanto, no necesita atributos alt. No tendría sentido añadir texto alternativo donde no existe contenido gráfico.
7.7. Uso de ARIA
ARIA complementa HTML cuando es necesario comunicar relaciones o estados que no quedan suficientemente claros. En esta aplicación se usa para:
- nombrar regiones mediante
aria-labelledby; - asociar la ayuda al campo mediante
aria-describedby; - reforzar el anuncio dinámico del resultado.
Los atributos de la región de resultado son explícitos, aunque <output> ya posee semántica propia. No convierten el código en inválido. En el apartado de revisión crítica se muestra una simplificación opcional basada en el principio «usar primero HTML nativo y añadir ARIA solo cuando aporte algo necesario».
8. E. Usabilidad
La usabilidad estudia hasta qué punto una interfaz resulta fácil de aprender, eficiente y poco propensa a errores. Una página puede ser técnicamente accesible y, aun así, resultar confusa; por eso conviene revisar ambos aspectos por separado.
8.1. Claridad de la tarea
El título, la etiqueta y el botón utilizan expresiones directas:
<h1 id="titulo-aplicacion" class="app__title">Calcular el cuadrado</h1>
<label class="calculation-form__label" for="numero">Introduce un número</label>
<button class="calculation-form__button" type="submit">Calcular el cuadrado</button>
La persona puede entender qué debe introducir y qué ocurrirá sin interpretar iconos ni términos innecesarios.
8.2. Prevención de errores
HTML limita el tipo de dato y evita el envío vacío:
type="number"
step="any"
inputmode="decimal"
aria-describedby="ayuda-numero"
required
La ayuda visible aclara que se admiten positivos, negativos y decimales. JavaScript añade mensajes específicos para un valor no finito y para un resultado que excede el rango representable.
Matiz importante: la validación nativa se produce antes del evento submit. Por eso, en condiciones normales, un campo vacío muestra el mensaje propio del navegador y no llega al primer mensaje de error de JavaScript.
8.3. Acción principal reconocible
El botón tiene texto descriptivo, color destacado, tamaño cómodo y estados visuales:
.calculation-form__button:hover {
background-color: #ffea3d;
transform: translateY(-0.12rem);
box-shadow: 0 0.47rem 0 var(--color-text);
}
.calculation-form__button:active {
transform: translateY(0.18rem);
box-shadow: 0 0.12rem 0 var(--color-text);
}
El movimiento comunica que el elemento responde a la interacción, pero no es imprescindible para entenderlo.
8.4. Retroalimentación después de la acción
Tras el envío, la zona de resultado cambia inmediatamente:
resultado.value = `${base}² = ${cuadrado}`;
El mensaje incluye el dato de entrada y la operación, no solo una cifra aislada. Para un número negativo, los paréntesis evitan ambigüedad matemática.
8.5. Adaptación a distintos tamaños
En pantallas anchas, formulario y resultado aparecen en dos columnas. En pantallas de hasta 42rem, pasan a una sola:
@media (max-width: 42rem) {
.app__content {
grid-template-columns: 1fr;
}
.result__output {
min-height: 6rem;
}
}
El orden sigue siendo lógico porque no se altera el DOM: primero entrada y acción; después resultado.
8.6. Coherencia y carga cognitiva
Las variables mantienen colores y radios coherentes. Los bloques están claramente separados, y la interfaz solo contiene una tarea principal. No se añaden opciones que distraigan del objetivo didáctico.
8.7. Limitación numérica que conviene explicar
JavaScript representa la mayoría de números con el formato de coma flotante binaria. Algunos decimales no pueden almacenarse exactamente. Por ello, una operación como el cuadrado de 0.1 puede mostrar más cifras de las esperadas. No es un error de sintaxis ni una vulnerabilidad, sino una limitación del modelo numérico.
La aplicación original no establece una política de redondeo. Añadirla sin saber cuántos decimales se desean podría ocultar precisión o cambiar el comportamiento didáctico. Por ese motivo, el tutorial conserva el cálculo original:
const cuadrado = numero ** 2;
9. F. Seguridad del código
La seguridad web consiste en impedir que los datos o acciones no confiables produzcan efectos no autorizados. En esta aplicación el riesgo es reducido porque no existen cuentas, servidor, almacenamiento ni comunicaciones de red. Aun así, incluye decisiones útiles que sirven para proyectos mayores.
9.1. Validación y tratamiento del dato
La validación se produce en dos capas del cliente:
- HTML aplica
type="number",step="any"yrequired. - JavaScript comprueba
Number.isFinite(numero)yNumber.isFinite(cuadrado).
if (!Number.isFinite(numero)) {
resultado.value = 'Introduce un número válido.';
campoNumero.focus();
return;
}
La comprobación JavaScript no confía únicamente en que el evento siempre proceda de un envío normal; también protege la función si otro código la invoca o manipula el campo.
9.2. Cliente frente a servidor
La validación del cliente mejora la experiencia, pero no constituye una frontera de seguridad: una persona puede modificar el HTML o ejecutar solicitudes sin usar la interfaz. Si el dato se enviase a un servidor, el servidor tendría que volver a validar el tipo, los límites y las reglas de negocio.
En la aplicación proporcionada no existe envío ni servidor. Por tanto, no falta una validación del servidor para su objetivo actual; simplemente sería obligatoria si en el futuro se almacenaran o procesaran datos fuera del navegador.
9.3. innerHTML frente a escritura textual
innerHTML analiza una cadena como marcado HTML. Puede producir una vulnerabilidad de inyección de HTML o XSS si la cadena contiene datos controlados por una persona y no se tratan correctamente. No es inseguro en todos los usos, pero resulta innecesario cuando solo se quiere mostrar texto.
La aplicación utiliza:
resultado.value = `${base}² = ${cuadrado}`;
La propiedad value de <output> actualiza la salida como texto. En un elemento genérico, la alternativa equivalente sería normalmente textContent. En este caso, value es más específica porque el destino es un <output>.
9.4. onclick frente a addEventListener()
El código no incluye un controlador en línea mediante el atributo onclick. En su lugar utiliza este fragmento original:
formularioCuadrado.addEventListener('submit', calcularCuadrado);
Un atributo onclick no crea por sí solo una vulnerabilidad. El riesgo aparece especialmente cuando su contenido se construye concatenando datos no confiables, porque esos datos podrían alterar el código del atributo. Además, los controladores en línea dificultan una Política de Seguridad de Contenidos estricta y mezclan estructura con comportamiento.
addEventListener() ofrece estas ventajas:
- mantiene JavaScript fuera del HTML;
- permite registrar varios manejadores sin sobrescribir una propiedad;
- facilita eliminar o reorganizar los eventos;
- evita construir código ejecutable dentro de cadenas HTML;
- en este caso, permite escuchar
submit, la acción semántica correcta.
9.5. Separación de responsabilidades
La aplicación mantiene:
- la estructura en
index.html; - la presentación en
style.css; - el comportamiento en
script.js.
Esta separación no elimina todas las vulnerabilidades, pero facilita revisar, probar y mantener cada responsabilidad. También evita estilos y eventos en línea.
9.6. Información sensible
Todo archivo enviado al navegador puede ser inspeccionado por quien visite la página. Nunca deben guardarse en HTML, CSS o JavaScript del cliente contraseñas, claves privadas, tokens secretos o credenciales de servidor.
El código proporcionado no contiene datos sensibles. El mensaje de error lanzado por throw new Error(...) describe una incidencia técnica general y no revela credenciales ni información privada.
9.7. Dependencias y superficie de ataque
La aplicación no incorpora bibliotecas de terceros ni realiza peticiones de red. Esto reduce la superficie de ataque y evita riesgos de cadena de suministro asociados a dependencias externas. No significa que toda aplicación sin dependencias sea automáticamente segura, pero sí que aquí hay menos componentes que mantener y auditar.
10. Revisión crítica del código
10.1. Resultado general de la revisión
Se ha comprobado la sintaxis del archivo JavaScript con el analizador de Node.js y no se han detectado errores. La revisión manual conjunta de HTML, CSS y JavaScript tampoco revela errores de sintaxis, fallos lógicos o riesgos de seguridad que impidan cumplir el objetivo declarado.
Los aspectos revisados son:
- Sintaxis: las estructuras, selectores, bloques y llamadas están correctamente cerrados.
- Lógica: el flujo impide la recarga, valida entrada y resultado, calcula con
** 2y presenta la salida. - Accesibilidad: hay semántica, etiqueta, ayuda asociada, teclado, foco visible, región de resultado y reducción de movimiento.
- Usabilidad: instrucciones claras, acción principal reconocible, validación y respuesta inmediata.
- Seguridad: no se usa
innerHTML, no hay controladores en línea, no hay datos sensibles ni dependencias. - Organización: los nombres son descriptivos y las clases siguen BEM de manera coherente.
- Código repetido o innecesario: no existe repetición significativa que justifique una refactorización.
Conclusión crítica: no es necesario reescribir los archivos para que la aplicación funcione correctamente. Las observaciones siguientes son matices didácticos o simplificaciones opcionales, no reparaciones obligatorias.
10.2. ARIA explícito en <output>: simplificación opcional
Código original
<output
id="resultado"
class="result__output"
name="cuadrado"
for="numero"
form="formulario-cuadrado"
role="status"
aria-live="polite"
aria-atomic="true"
>El resultado aparecerá aquí.</output>
Análisis
<output> ya es un elemento semántico destinado a resultados y dispone de semántica accesible propia. Declarar además role="status", aria-live="polite" y aria-atomic="true" hace explícita la intención y puede ser útil en un ejemplo docente o como refuerzo de compatibilidad. No es un error ni un riesgo.
Si se aplica de forma estricta el principio de usar la menor cantidad de ARIA necesaria, se podría simplificar el elemento. Esta decisión debe probarse con los navegadores y tecnologías de asistencia que necesite el proyecto.
Versión simplificada opcional —no sustituye al código original del tutorial—
<output
id="resultado"
class="result__output"
name="cuadrado"
for="numero"
form="formulario-cuadrado"
>El resultado aparecerá aquí.</output>
Justificación
La versión simplificada confía primero en la semántica nativa. La versión original, en cambio, documenta de manera explícita el comportamiento deseado de la región. Ambas conservan la misma finalidad; no se recomienda cambiarla sin realizar pruebas de accesibilidad.
10.3. Precisión de los decimales: limitación, no error lógico
Código original
const cuadrado = numero ** 2;
Análisis
El cálculo es correcto, pero ciertos decimales pueden mostrar una representación extensa debido a la coma flotante binaria de JavaScript. Una corrección concreta requeriría decidir primero una regla que el enunciado no proporciona: número de decimales, redondeo, cifras significativas o conservación de toda la precisión disponible.
Decisión adoptada
Se mantiene el fragmento original. Aplicar toFixed() arbitrariamente cambiaría la presentación y podría redondear resultados que el ejercicio pretende observar. Aquí es más riguroso explicar la limitación que inventar una política.
10.4. Validación HTML y JavaScript
Código original
<input
class="calculation-form__input"
type="number"
id="numero"
name="numero"
step="any"
inputmode="decimal"
aria-describedby="ayuda-numero"
required
/>
if (!Number.isFinite(numero)) {
resultado.value = 'Introduce un número válido.';
campoNumero.focus();
return;
}
Análisis y justificación
Las dos validaciones no son una repetición innecesaria. La primera proporciona respuesta nativa antes del envío; la segunda protege la función ante invocaciones o cambios no previstos. Se conserva este enfoque por ser robusto y fácil de explicar.
10.5. Nombres y mantenimiento
Los nombres formularioCuadrado, campoNumero, resultado, numero, cuadrado y base expresan su función. Las clases BEM reflejan la pertenencia entre bloques y elementos. Los comentarios son más abundantes de lo habitual en una aplicación de producción, pero resultan apropiados para el objetivo docente y no deben considerarse código innecesario en este contexto.
11. Resumen y cierre
La aplicación parte de una estructura HTML semántica, aplica una presentación organizada mediante BEM y añade el comportamiento con JavaScript no intrusivo. El recorrido completo puede resumirse así:
- HTML crea el formulario y el resultado.
- CSS organiza la interfaz, diferencia estados y adapta la distribución.
- JavaScript espera el envío del formulario.
- Obtiene el dato con
valueAsNumber. - Comprueba que tanto la entrada como el resultado sean finitos.
- Calcula con
numero ** 2. - Muestra una expresión clara mediante la propiedad
valuede<output>.
Buenas prácticas aplicadas
- declarar idioma, codificación y
viewport; - usar
<main>,<header>,<form>,<label>,<button>,<section>y<output>según su significado; - mantener una jerarquía
h1→h2; - asociar etiqueta, ayuda, campo, formulario y resultado;
- utilizar controles nativos compatibles con teclado;
- mostrar un foco visible;
- respetar
prefers-reduced-motion; - organizar CSS mediante variables y nombres BEM;
- cargar el script con
defer; - usar constantes descriptivas y comprobar las referencias al DOM;
- escuchar
submitconaddEventListener(); - validar antes de calcular;
- usar escritura textual en lugar de
innerHTML; - separar estructura, presentación y comportamiento.
Errores habituales que deben evitarse
- romper las rutas a CSS o JavaScript;
- hacer que
for,id,aria-describedby,formoaria-labelledbyapunten a valores inexistentes; - usar encabezados por su apariencia y no por su nivel lógico;
- olvidar
step="any"si se quieren admitir decimales; - registrar
calcularCuadrado()con paréntesis enaddEventListener(); - olvidar
preventDefault(); - tratar el valor textual del campo como si ya fuera un número;
- comprobar
NaNmediante una comparación directa; - eliminar el indicador de foco sin proporcionar otro;
- insertar datos con
innerHTMLcuando solo deben mostrarse como texto; - confiar en la validación del cliente si algún día se añade un servidor;
- guardar secretos en archivos que se envían al navegador.
12. Actividad de comprobación
Responde sin consultar el código y después comprueba tus respuestas:
- ¿Qué responsabilidad corresponde a HTML, cuál a CSS y cuál a JavaScript?
- ¿Por qué
for="numero"yid="numero"deben coincidir? - ¿Qué diferencia existe entre
type="number",step="any"yrequired? - ¿Por qué se escucha el evento
submitdel formulario en lugar de limitarse al clic del botón? - ¿Qué ocurriría si se escribiera
calcularCuadrado()como segundo argumento deaddEventListener()? - ¿Qué devuelve
valueAsNumbercuando el campo no contiene un número utilizable? - ¿Por qué se comprueba también
Number.isFinite(cuadrado)después de calcular? - ¿Qué ventaja de seguridad aporta escribir en
resultado.valueen vez de usarinnerHTML? - ¿Qué cambio realiza la consulta
@media (max-width: 42rem)? - ¿Para qué sirve
prefers-reduced-motion?
Soluciones orientativas
- HTML estructura y da significado; CSS presenta; JavaScript gestiona el comportamiento.
- Porque
foridentifica el campo etiquetado; si no coincide, se pierde esa asociación. typedefine el tipo de control,steppermite cualquier incremento decimal yrequiredimpide el envío vacío.- Porque
submitrepresenta el envío completo y también funciona con teclado. - La función se ejecutaría inmediatamente durante el registro, en vez de esperar al evento.
- Devuelve
NaN. - Porque una entrada finita muy grande puede producir
Infinityal elevarse al cuadrado. - Evita interpretar el contenido como marcado o código HTML.
- Sustituye las dos columnas por una y reduce la altura mínima del resultado.
- Para respetar la preferencia de las personas que necesitan reducir o evitar animaciones.
13. Lista de verificación
Antes de dar por terminada la aplicación, comprueba lo siguiente:
Si todos los puntos se cumplen, la aplicación conserva su semántica, resulta utilizable con teclado, responde a distintos tamaños de pantalla y ejecuta correctamente el cálculo previsto.