In LWC, data flows from parent to child via public properties declared with @api. The template binds parent values to child attributes the same way HTML always has.
The basic pattern
Child component declares the public surface:
// childCard.js
import { LightningElement, api } from 'lwc';
export default class ChildCard extends LightningElement {
@api title;
@api items = []; // default value
@api variant = 'base';
}
<!-- childCard.html -->
<template>
<div class={variant}>
<h2>{title}</h2>
<ul>
<template for:each={items} for:item="item">
<li key={item.id}>{item.name}</li>
</template>
</ul>
</div>
</template>
Parent binds values:
<!-- parentList.html -->
<template>
<c-child-card
title={cardTitle}
items={contactList}
variant="emphasis">
</c-child-card>
</template>
// parentList.js
import { LightningElement } from 'lwc';
export default class ParentList extends LightningElement {
cardTitle = 'Active Contacts';
contactList = [
{ id: '1', name: 'Ada Lovelace' },
{ id: '2', name: 'Grace Hopper' }
];
}
Three rules to remember:
- kebab-case in HTML, camelCase in JS. A child’s
firstNameisfirst-namein markup. Salesforce is consistent — base components likelightning-inputfollow the same convention. - Attributes are strings; properties are typed. Curly-brace expressions (
title={cardTitle}) pass the actual JS value. A literalvariant="emphasis"passes the string"emphasis". - Public properties are reactive. When the parent changes
cardTitle, the child re-renders automatically — no event needed.
Calling a public method on a child
@api works on methods too. The parent grabs the child via a ref or query and invokes it directly:
// modalChild.js
import { LightningElement, api } from 'lwc';
export default class ModalChild extends LightningElement {
@api open() { this.isOpen = true; }
@api close() { this.isOpen = false; }
isOpen = false;
}
<template>
<c-modal-child lwc:ref="modal"></c-modal-child>
<button onclick={handleOpen}>Open</button>
</template>
handleOpen() {
this.refs.modal.open();
}
lwc:ref is the modern way to get at child elements — no more this.template.querySelector(...) for components you’ve named yourself.
What you shouldn’t do
Don’t pass mutable state down and expect children to mutate it. LWC enforces a one-way data flow: the child treats @api props as read-only. If a child needs to push a change back, it raises an event (see child-to-parent communication).
// In the child — DON'T do this:
@api items = [];
addItem() {
this.items.push('new'); // mutates the parent's array. Anti-pattern.
}
Don’t use @api for everything. Internal state should stay private — public surface is API contract you have to maintain. A useful test: if a different team consumed this component, would they need to know about this property? If not, leave it private.
Boolean props and the is-active trap
Boolean @api props receive a subtle gotcha. In HTML, the presence of an attribute means true:
<c-toggle is-active></c-toggle> <!-- true -->
<c-toggle is-active={state}></c-toggle> <!-- the JS value of state -->
<c-toggle is-active="false"></c-toggle> <!-- the STRING "false" — truthy! -->
The third form is the bug. If a value can be dynamic, always bind it with curly braces.
Interviewer-pleasing extras
- Property changes call a setter, if you define one. Use a
setaccessor to validate or transform incoming values before storing them. @apiproperties can’t share a name with reserved attributes likeclass,is,style,slot,tabindex— the framework will throw at compile time.- For complex data structures, prefer passing an object or array reference over many small props. It’s easier to evolve the contract over time.
Verified against: LWC Developer Guide — Communicate Between Components. Last reviewed 2026-05-17 for Spring ‘26 release.