The properties of <task-card> declared so far use simple types: String, Number and Boolean. But a real component often needs to handle richer data types: collections, structured objects, or even types that do not exist natively in HTML, such as a date. This lesson goes through the full catalog of types that Lit knows how to convert out of the box between attribute and property, dwells on a classic problem with Boolean, and teaches you how to write a custom converter for a type of your own, which will be applied to a new fechaLimite property on <task-card>.

Contents

  1. The underlying problem: HTML only understands text
  2. The types supported out of the box: String, Number, Boolean, Array, Object
  3. The classic Boolean problem with attributes
  4. When a custom converter is needed
  5. Writing a custom converter
  6. Applying the converter to fechaLimite on <task-card>

  1. The underlying problem: HTML only understands text

Before getting into the catalog of types, it is worth pinning down the reason this whole conversion mechanism exists. An HTML attribute, by definition of the web platform standard itself, is always a string of text. When you write <task-card prioridad="5">, the browser does not internally store the number 5; it literally stores the text "5". This holds for any attribute of any HTML element, not only for Custom Elements: <input type="number" value="5"> also stores "5" as text in the value attribute, even though the field visually behaves as numeric.

In JavaScript, however, you want to work with the actual data types: a real number so you can do arithmetic with prioridad, a real boolean so you can use urgente directly in a condition, and so on. Lit's responsibility, when you declare type in static properties, is exactly to bridge both worlds: convert the attribute's text into the declared JavaScript type when the attribute changes, and convert the JavaScript value back into text when it needs to be reflected in the attribute (something detailed in the next lesson, on reflection).

  1. The types supported out of the box: String, Number, Boolean, Array, Object

Lit recognizes, out of the box, five values for the type option, each with its own conversion logic from the attribute's text:

type How it converts from attribute (text) to property Attribute example Resulting value in JS
String The text is used as is, with no transformation titulo="Revisar el PR" "Revisar el PR"
Number Number(valorDelAtributo) is applied prioridad="5" 5 (number)
Boolean Presence/absence of the attribute, not its text content (detailed in section 3) urgente true
Array The attribute's text is parsed as JSON with JSON.parse etiquetas='["urgente","cliente"]' ['urgente', 'cliente']
Object Same as Array, via JSON.parse on the attribute's text metadatos='{"autor":"Ana"}' { autor: 'Ana' }

The first three types (String, Number, Boolean) are the ones used in <task-card> so far, and their behavior is reasonably intuitive except for the Boolean case, explained in the next section. The last two (Array, Object) are less commonly set directly as text in an HTML attribute — hand-writing valid JSON inside an HTML tag is awkward and prone to quote-escaping mistakes — but they feel perfectly natural when the property is assigned from JavaScript, without ever going through its text representation:

// Direct assignment from JavaScript: no manual JSON.parse is needed,
// because it never goes through the attribute's text representation.
tarjeta.etiquetas = ['urgente', 'cliente'];

In fact, this is the most common pattern in real applications built with Lit: compound types (Array, Object) are almost always assigned from JavaScript, while HTML attributes in the markup itself are mostly reserved for simple types (String, Number, Boolean), which are the ones that make sense to write by hand in an HTML template.

  1. The classic Boolean problem with attributes

The Boolean type deserves a separate explanation because its behavior surprises anyone seeing it for the first time, and it is a frequent source of bugs. The Boolean conversion does not look at the attribute's text content, only at whether the attribute is present or absent on the tag, following the same convention used by HTML's native boolean attributes (disabled, checked, required...).

<!-- urgente vale true: el atributo está presente, con cualquier contenido o sin ninguno -->
<task-card urgente></task-card>
<task-card urgente="true"></task-card>
<task-card urgente="false"></task-card>
<task-card urgente=""></task-card>

<!-- urgente vale false: el atributo, sencillamente, no está -->
<task-card></task-card>

This is, by far, the most common mistake when working with Boolean properties in Lit: writing urgente="false" expecting the property to be false, when in reality the attribute is present (it holds the text "false", but it is there), so Lit interprets it as true. The only way for a Boolean property to be false through an HTML attribute is for that attribute not to appear at all on the tag.

This convention is not some quirk invented by Lit: it is exactly how the browser's native boolean attributes work. <input disabled="false"> still disables the field, because the disabled attribute is present; to enable it, you have to remove the attribute entirely, not set it to "false". Lit simply respects this same convention to stay consistent with the rest of the web platform.

From JavaScript, on the other hand, there is no such ambiguity: assigning tarjeta.urgente = false works exactly as expected, because there is no text involved there, only the real JavaScript boolean value.

  1. When a custom converter is needed

The five types from section 2 cover the vast majority of common cases, but there are data types that do not fit naturally into any of them. The example used in this lesson is a date: if <task-card> needs a fechaLimite property, what type should it declare?

  • Declaring it as String would work only halfway: the attribute would work, but the JavaScript property would just be a text string, with none of the capabilities of a Date object (comparing dates, calculating how many days are left, formatting in different ways).
  • Declaring it as Object does not fit well either: a JavaScript Date object does not serialize usefully with JSON.stringify (it produces a string in ISO format inside quotes, but running JSON.parse on it back does not automatically reconstruct a Date object, just a plain text string).

For situations like this, Lit lets you replace the automatic conversion based on type with a fully custom conversion function, through the converter option.

  1. Writing a custom converter

A converter is an object with up to two functions: fromAttribute, which converts the attribute's text into the property's value, and toAttribute, which does the reverse (only needed if reflect: true is also used, explained in the next lesson). For the fechaLimite case, you want to convert between a text string in AAAA-MM-DD format (the usual format for a date attribute in HTML, the same one used by <input type="date">) and a real JavaScript Date object:

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    // valorDelAtributo is the attribute's text, or null if it is not present
    if (!valorDelAtributo) {
      return null;
    }
    return new Date(valorDelAtributo);
  },
  toAttribute(valorDeLaPropiedad) {
    // valorDeLaPropiedad is the property's Date object (or null)
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0]; // "AAAA-MM-DD"
  },
};

Let's analyze each function separately. fromAttribute receives the attribute's text exactly as it is in the HTML (for example, "2026-07-15") and must return the value the JavaScript property will hold: here, a Date object built with new Date(valorDelAtributo), which correctly parses a string in AAAA-MM-DD format. toAttribute does the opposite: it receives the property's Date object and must return the text that will be written to the attribute if the property is reflected; here toISOString() is used (which returns a full date with time, in 2026-07-15T00:00:00.000Z format) and it is trimmed with split('T')[0] to keep only the date part.

Both functions first check whether the input value is "falsy" (null, undefined, empty string) and return null in that case, to avoid errors if the property does not yet have a date assigned; this is a reasonable precaution in any custom converter, since Lit can call these functions at moments when the value has not yet been set.

  1. Applying the converter to fechaLimite on <task-card>

With the converter already written, it gets applied to the property by specifying it in the configuration object of static properties, instead of (or alongside) type:

import { LitElement, html } from 'lit';

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    if (!valorDelAtributo) {
      return null;
    }
    return new Date(valorDelAtributo);
  },
  toAttribute(valorDeLaPropiedad) {
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0];
  },
};

class TaskCard extends LitElement {
  static properties = {
    titulo: { type: String },
    estado: { type: String },
    prioridad: { type: Number },
    urgente: { type: Boolean },
    expandida: { state: true },
    fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
  };

  constructor() {
    super();
    this.titulo = 'Tarea sin título';
    this.estado = 'pendiente';
    this.prioridad = 3;
    this.urgente = false;
    this.expandida = false;
    this.fechaLimite = null;
  }

  renderInsigniaEstado() {
    if (this.estado === 'hecha') {
      return html`<span class="insignia insignia--hecha">✓ Hecha</span>`;
    }
    if (this.estado === 'en-progreso') {
      return html`<span class="insignia insignia--progreso">◐ En progreso</span>`;
    }
    return html`<span class="insignia insignia--pendiente">○ Pendiente</span>`;
  }

  renderFechaLimite() {
    if (!this.fechaLimite) {
      return '';
    }
    return html`<p>Fecha límite: ${this.fechaLimite.toLocaleDateString('es-ES')}</p>`;
  }

  render() {
    return html`
      <article>
        <h3>${this.titulo}</h3>
        ${this.renderInsigniaEstado()}
        <p>Prioridad: ${this.prioridad}</p>
        ${this.renderFechaLimite()}
        ${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
      </article>
    `;
  }
}

customElements.define('task-card', TaskCard);

With this declaration, <task-card fecha-limite="2026-07-15"> makes this.fechaLimite, inside the component, a real Date object, not a text string: you can call this.fechaLimite.toLocaleDateString('es-ES') (as done in renderFechaLimite) or any other Date method, with no need to manually convert anything in render(). This is exactly the value of a custom converter: it moves the conversion responsibility to the single place where the property is declared, instead of repeating it every time the value is read.

Note also that attribute: 'fecha-limite' has been set explicitly, applying what was learned in the module's first lesson about kebab-case attribute names for properties with a compound camelCase name.

Common Mistakes and Tips

  • Writing urgente="false" expecting the property to be false: as explained in section 3, with type: Boolean what matters is the presence or absence of the attribute, not its text content; for it to be false from HTML, the attribute has to be omitted entirely.
  • Trying to declare type: Date expecting Lit to support it out of the box: Date is not one of the five automatically recognized types (section 2); if you declare { type: Date }, Lit will treat it as equivalent to String with no special conversion at all, and this.fechaLimite would simply be the attribute's text, not a Date object. Unsupported types need a custom converter, as shown in section 5.
  • Forgetting to check for null or empty values inside fromAttribute/toAttribute: if the converter from section 5 did not check if (!valorDelAtributo) at the start, calling new Date(null) or new Date(undefined) would produce an invalid date (Invalid Date) instead of clearly failing or returning null, which can cause confusing errors later in the template.
  • Passing malformed JSON to an Array or Object attribute: hand-writing etiquetas='["urgente", "cliente"]' inside an HTML attribute is prone to quoting mistakes (the JSON's double quotes clash with the attribute's own quotes if not handled carefully); in practice, for compound types it is usually more reliable to assign the property from JavaScript, as noted in section 2.

Exercises

  1. Without using any custom converter, declare an etiquetas property of type Array on <task-card>, with initial value [], and show it in the template as a comma-separated list (this.etiquetas.join(', ')). Check, by assigning it from the browser console with elemento.etiquetas = ['cliente', 'urgente'], that the template updates.
  2. Take the conversorDeFecha converter from section 5 and add an extra check in fromAttribute that, if the resulting new Date(...) is invalid (checkable with Number.isNaN(fecha.getTime())), returns null instead of an invalid date.
  3. Explain in your own words why <task-card urgente="false"></task-card> does not disable the urgency warning, and what should be written instead so that urgente is false.

Solutions

static properties = {
  // ...previous properties...
  etiquetas: { type: Array },
};

constructor() {
  super();
  // ...
  this.etiquetas = [];
}

render() {
  return html`
    <article>
      <h3>${this.titulo}</h3>
      <p>Etiquetas: ${this.etiquetas.join(', ')}</p>
    </article>
  `;
}

Running elemento.etiquetas = ['cliente', 'urgente'] from the console fully reassigns the etiquetas property (the existing array is not mutated), so the reactive setter fires normally and the template shows "Etiquetas: cliente, urgente".

const conversorDeFecha = {
  fromAttribute(valorDelAtributo) {
    if (!valorDelAtributo) {
      return null;
    }
    const fecha = new Date(valorDelAtributo);
    if (Number.isNaN(fecha.getTime())) {
      return null;
    }
    return fecha;
  },
  toAttribute(valorDeLaPropiedad) {
    if (!valorDeLaPropiedad) {
      return null;
    }
    return valorDeLaPropiedad.toISOString().split('T')[0];
  },
};
  1. With type: Boolean, Lit determines the property's value only by whether the attribute is present on the tag, without looking at its text content; since urgente="false" still has the urgente attribute present (with the text "false" inside, but present nonetheless), Lit interprets it as true, just as happens with native attributes like disabled. For urgente to be false, the tag would have to be written without the attribute at all: <task-card></task-card>.

Conclusion

In this lesson you have gone through the full catalog of types that Lit converts out of the box between attribute and property (String, Number, Boolean, Array, Object), dwelt on the particular behavior of Boolean with attributes — presence versus text content —, and learned how to write a custom converter for types that do not fit that catalog, applying it to a new fechaLimite property on <task-card> that now handles real Date objects instead of plain text strings.

This whole conversion mechanism assumes a one-way flow: from the attribute to the property. In the next lesson, "Attributes vs Properties and Reflection", you will close the loop by also understanding the reverse path — when and why it is worth having a property change reflected back into the attribute — and you will apply everything learned in this module to turn tareas, on <task-list>, into a reactive property of type Array, finally getting every <task-card> on the board to show the data for its own task.

Lit Course

Module 1: Introduction to Lit and Web Components

Module 2: Reactive Templates and Rendering

Module 3: Reactive Properties and State

Module 4: Styling Lit Components

Module 5: Events and Component Communication

Module 6: Lifecycle and Advanced Behavior

Module 7: Directives and Advanced Template Features

Module 8: Integration, Interoperability and Deployment

Module 9: Testing and Best Practices

Module 10: Project: Building TaskFlow

© Copyright 2026. All rights reserved