How to build a Message Center page in Tecton: a two-panel layout with a message list on the left and a selected message detail plus response form on the right.
A Message Center page presents a user's messages in a two-panel layout. The left panel holds a scrollable list of messages with pagination. The right panel shows the full content of a selected message, followed by a response form. This example uses static composition: the list and detail do not update on selection. The only live behavior is the response form, which validates input on submit and resets on demand. A real application would add selection wiring and submit the response to an API.
A suspicious transaction was detected on your account at 09:15 AM. Your digital banking access has been temporarily restricted for your security until you review and respond to this message.
This is a standard practice of the Fraud Department to help minimize suspicious and illegal activity. We understand this may disrupt your regular spending until this is resolved.
Please review the transaction information below and respond to this message or contact our Fraud Department immediately at (800) 555-0156 in order to restore regular account activity.
<q2-grid columns="1" md-columns="5" gap="comfortable">
<q2-grid-area md-column-span="2">
<q2-card type="non-clickable">
<q2-list label="Messages">
<q2-item clickable>
<div slot="header">Potential Fraud Alert</div>
<q2-detail label="From" description="Fraud Department" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-06-07" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
<q2-item clickable>
<div slot="header">You Got Paid</div>
<q2-detail label="From" description="Bill Pay Services" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-06-05" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
<q2-item clickable>
<div slot="header">You Got Paid</div>
<q2-detail label="From" description="Bill Pay Services" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-05-30" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
<q2-item clickable>
<div slot="header">Transfer Received</div>
<q2-detail label="From" description="Payroll Services" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-05-20" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
<q2-item clickable>
<div slot="header">Payment Due</div>
<q2-detail label="From" description="Bill Pay Services" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-05-10" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
</q2-list>
<q2-pagination record-type="Messages" total="23" page="1" per-page="5" records-only>
</q2-pagination>
</q2-card>
</q2-grid-area>
<q2-grid-area md-column-span="3">
<h3>Potential Fraud Alert</h3>
<q2-detail label="From" description="Fraud Department">
</q2-detail>
<q2-detail label="Received">
<q2-relative-time date="2026-06-07" unit="day" message-format="long"></q2-relative-time>
</q2-detail>
<div style="margin-block: var(--app-scale-10x);">
<p>A suspicious transaction was detected on your account at 09:15 AM. Your digital banking access has been temporarily restricted for your security until you review and respond to this message.</p>
<p>This is a standard practice of the Fraud Department to help minimize suspicious and illegal activity. We understand this may disrupt your regular spending until this is resolved.</p>
<p>Please review the transaction information below and respond to this message or contact our Fraud Department immediately at (800) 555-0156 in order to restore regular account activity.</p>
<h4>Transaction Details</h4>
<q2-detail label="Date" description="June 06, 2026"></q2-detail>
<q2-detail label="Time" description="4:45 PM (PST)"></q2-detail>
<q2-detail label="Transaction Source" description="totally-not-suspicious.com"></q2-detail>
<q2-detail label="Account" description="Family Checking"></q2-detail>
<q2-detail label="Amount">
<q2-currency amount="91.87"></q2-currency>
</q2-detail>
</div>
<q2-form>
<q2-textarea id="reply-textarea" label="Response"></q2-textarea>
</q2-form>
<q2-action-group>
<q2-btn id="send-btn" intent="workflow-primary">
<span>Send Response</span>
<q2-icon type="paper-plane"></q2-icon>
</q2-btn>
<q2-btn id="reset-btn" intent="workflow-secondary">
<span>Reset</span>
<q2-icon type="refresh"></q2-icon>
</q2-btn>
</q2-action-group>
</q2-grid-area>
</q2-grid>
<script>
// Element references.
const replyTextarea = document.querySelector('#reply-textarea');
const sendBtn = document.querySelector('#send-btn');
const resetBtn = document.querySelector('#reset-btn');
// Field guidance lives in the hints array, not a placeholder, so it stays visible.
replyTextarea.hints = ['Please provide a response relating to the authenticity of the above transaction.'];
// Track the typed value. q2-textarea only updates its own `value` on the change
// (blur) event, so reading it on click would miss text the user just typed.
let replyValue = '';
// Store the latest value and clear the error as soon as the user types.
replyTextarea.addEventListener('tctInput', event => {
replyValue = event?.detail?.value ?? '';
if (replyTextarea.errors?.length) replyTextarea.errors = [];
});
// Validate on submit: require a non-empty response. Stop at validation.
// A real application would submit the response to an API when this passes.
sendBtn.addEventListener('tctClick', () => {
if (!replyValue.trim()) {
replyTextarea.errors = ['Please enter a response before sending.'];
return;
}
});
// Reset clears the tracked value, the textarea value, and any validation error.
resetBtn.addEventListener('tctClick', () => {
replyValue = '';
replyTextarea.value = '';
replyTextarea.errors = [];
});
</script>
The layout is a two-column q2-grid. On wide viewports the left column spans 2 units and holds the message list; the right column spans 3 units and holds the message detail and response form. On narrow viewports the grid stacks to a single column.
The list and detail are static. The only script in this example drives the response form: it validates input when the user clicks Send Response and resets the field when the user clicks Reset.
The left panel wraps a q2-list and a q2-pagination inside a q2-card. Each message is a q2-item with a header, a sender detail, and a received date rendered by q2-relative-time. The pagination shows the total count and current page without page-navigation controls (records-only).
<q2-list label="Messages">
<q2-item clickable>
<div slot="header">Potential Fraud Alert</div>
<q2-detail label="From" description="Fraud Department" slot="body"></q2-detail>
<q2-detail slot="body" label="Received">
<q2-relative-time date="2026-06-07" unit="day"></q2-relative-time>
</q2-detail>
</q2-item>
</q2-list>
<q2-pagination record-type="Messages" total="23" page="1" per-page="5" records-only>
</q2-pagination>The right panel opens with the message subject as an <h3>, followed by sender and date metadata in q2-detail elements. Generous vertical spacing, rather than borders, separates the metadata, the message body, and the response form. Structured data, such as transaction details, is listed as additional q2-detail elements.
The response form uses a q2-form wrapper with a single q2-textarea. Its guidance lives in the textarea's hints array rather than a placeholder, so it stays visible while the user writes. The q2-action-group below it holds the primary Send Response button and a secondary Reset button.
Validation runs when the user clicks Send Response. The textarea's value is tracked in replyValue as the user types, because q2-textarea only updates its own value on the change (blur) event, so reading it on click could miss the latest input. If replyValue is empty, the script sets the textarea's errors property to show an inline error. Once the user types anything, the error clears immediately.
let replyValue = '';
replyTextarea.addEventListener('tctInput', event => {
replyValue = event?.detail?.value ?? '';
if (replyTextarea.errors?.length) replyTextarea.errors = [];
});
sendBtn.addEventListener('tctClick', () => {
if (!replyValue.trim()) {
replyTextarea.errors = ['Please enter a response before sending.'];
return;
}
});Setting errors to a non-empty array activates the textarea's error state. Setting it back to an empty array clears it. The tctInput listener clears the error as the user types so feedback on correction is immediate.
Reset returns the textarea to its empty state and clears any pending error.
resetBtn.addEventListener('tctClick', () => {
replyValue = '';
replyTextarea.value = '';
replyTextarea.errors = [];
});