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
- The underlying problem: HTML only understands text
- The types supported out of the box:
String,Number,Boolean,Array,Object - The classic
Booleanproblem with attributes - When a custom converter is needed
- Writing a custom
converter - Applying the converter to
fechaLimiteon<task-card>
- 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).
- The types supported out of the box:
String, Number, Boolean, Array, Object
String, Number, Boolean, Array, ObjectLit 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.
- The classic
Boolean problem with attributes
Boolean problem with attributesThe 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.
- 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
Stringwould work only halfway: the attribute would work, but the JavaScript property would just be a text string, with none of the capabilities of aDateobject (comparing dates, calculating how many days are left, formatting in different ways). - Declaring it as
Objectdoes not fit well either: a JavaScriptDateobject does not serialize usefully withJSON.stringify(it produces a string in ISO format inside quotes, but runningJSON.parseon it back does not automatically reconstruct aDateobject, 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.
- Writing a custom
converter
converterA 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.
- Applying the converter to
fechaLimite on <task-card>
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 befalse: as explained in section 3, withtype: Booleanwhat matters is the presence or absence of the attribute, not its text content; for it to befalsefrom HTML, the attribute has to be omitted entirely. - Trying to declare
type: Dateexpecting Lit to support it out of the box:Dateis not one of the five automatically recognized types (section 2); if you declare{ type: Date }, Lit will treat it as equivalent toStringwith no special conversion at all, andthis.fechaLimitewould simply be the attribute's text, not aDateobject. Unsupported types need a customconverter, as shown in section 5. - Forgetting to check for null or empty values inside
fromAttribute/toAttribute: if the converter from section 5 did not checkif (!valorDelAtributo)at the start, callingnew Date(null)ornew Date(undefined)would produce an invalid date (Invalid Date) instead of clearly failing or returningnull, which can cause confusing errors later in the template. - Passing malformed JSON to an
ArrayorObjectattribute: hand-writingetiquetas='["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
- Without using any custom converter, declare an
etiquetasproperty of typeArrayon<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 withelemento.etiquetas = ['cliente', 'urgente'], that the template updates. - Take the
conversorDeFechaconverter from section 5 and add an extra check infromAttributethat, if the resultingnew Date(...)is invalid (checkable withNumber.isNaN(fecha.getTime())), returnsnullinstead of an invalid date. - 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 thaturgenteisfalse.
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];
},
};- 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; sinceurgente="false"still has theurgenteattribute present (with the text"false"inside, but present nonetheless), Lit interprets it astrue, just as happens with native attributes likedisabled. Forurgenteto befalse, 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
- What are Web Components and why Lit?
- Setting Up the Development Environment
- Your First Lit Component
- Anatomy of a Lit Component
Module 2: Reactive Templates and Rendering
- Lit's Template Engine
- Expressions and Interpolation in Templates
- Conditional Rendering
- List Rendering
- The Rendering Cycle
Module 3: Reactive Properties and State
- Reactive Properties
- Internal State with @state
- Types of Properties and Custom Converters
- Attributes vs Properties and Reflection
Module 4: Styling Lit Components
- Encapsulated CSS with Shadow DOM
- Shared Styles Between Components
- Custom CSS Properties and Theming
- Slots and Styling Distributed Content
Module 5: Events and Component Communication
- Handling DOM Events in Templates
- Custom Events: Communication from Child to Parent
- Communication from Parent to Child with Properties
- Communication Patterns Between Sibling Components
Module 6: Lifecycle and Advanced Behavior
- Lifecycle Callbacks
- Reactive Hooks: willUpdate, updated, and firstUpdated
- Reactive Controllers
- Mixins and Composing Behavior
Module 7: Directives and Advanced Template Features
- Built-in Directives: classMap, styleMap and ifDefined
- Custom Directives
- Asynchronous Rendering with until
- Shared Context with @lit/context
Module 8: Integration, Interoperability and Deployment
- Using Lit Components in Plain HTML
- Integrating Lit with React, Vue, and Angular
- Server-Side Rendering with @lit-labs/ssr
- Bundling, Publishing, and TypeScript
Module 9: Testing and Best Practices
- Unit Tests with Web Test Runner
- Accessibility in Web Components
- Performance and Optimization
- Common Patterns and Anti-patterns
