Skip to content
Nana UI

Rental

Approval Card

One thing waiting on the owner — a repair quote, a fee waiver, a refund, a new tenancy — with what it costs, who asked, and the decision to make.

View .md Download

Approving is one press. Rejecting asks why, because the person who asked will read the answer; require-reason="all" asks on approval too, for a portfolio that wants every decision explained. Once decided, the card becomes the record of the decision: who made it, when, and what they said.

The card does not decide anything itself. It fires the decision; the app saves it and sets status. Set busy while the save is in flight.

The element has failed twice this month. Quote from Jua Plumbers, fitted Thursday.

Reject opens a field for the reason and will not send without one: the caretaker who asked will read it. Escape or Cancel puts the buttons back. allow-changes adds a third answer, for a request that is nearly right.

Paid on the 6th; the M-Pesa reversal on the 5th was the bank's error. Statement attached.

The app saves the decision and sets status, decided-by, decided-at and decision-note. The buttons give way to the record.

const card = document.querySelector("nana-approval-card");
card.addEventListener("nana-approve", async (e) => {
card.busy = true;
await api.decide(id, "approved", e.detail.reason);
Object.assign(card, { busy: false, status: "approved", decidedBy: me.name, decidedAt: new Date().toISOString() });
});
card.addEventListener("nana-reject", (e) => api.decide(id, "rejected", e.detail.reason));

require-reason decides which answers need words: reject (the default), all, or none. Asking for changes always does.

npm install @nana-tec/ui-components

Or copy the source in and own it:

npx @nana-tec/ui add approval-card
import "@nana-tec/ui-components/approval-card";
<nana-approval-card
kind="Repair quote"
heading="Replace the geyser in Unit 4B"
amount="18500"
currency="KES"
requester="Otieno, caretaker"
requested-at="2026-10-07T08:30:00Z"
>
The element has failed twice this month. Quote from Jua Plumbers, fitted Thursday.
</nana-approval-card>
const el = document.querySelector("nana-approval-card");
el.addEventListener("nana-approve", (e) => console.log(e.detail));
el.addEventListener("nana-reject", (e) => console.log(e.detail));
el.addEventListener("nana-request-changes", (e) => console.log(e.detail));

Properties

PropertyAttributeTypeDefaultDescription
actionsAlignactions-align'start' | 'end''end'Which side the decision buttons sit on
allowChangesallow-changesbooleanfalseOffer to send the request back with changes, as well as approve and reject
amountamountnumber | undefined—What it costs; leave empty for a request with no amount
busybusybooleanfalseA decision is being saved
currencycurrencystring'KES'
decidedAtdecided-atstring''When the decision was made, as an ISO date
decidedBydecided-bystring''Who made the decision
decisionNotedecision-notestring''What was said with the decision
dueduestring''When a decision is needed by, as an ISO date
headingheadingstring''The request in a line
headingLevelheading-levelnumber3Level of the heading in the page's outline
kindkindstring''What kind of request this is
requestedAtrequested-atstring''When they asked, as an ISO date
requesterrequesterstring''Who asked
requireReasonrequire-reasonReasonRule'reject'Which decisions need a reason
statusstatusApprovalStatus'pending'Where the request stands

Events

EventDescription
nana-approveThe owner approved (detail.reason)
nana-rejectThe owner rejected (detail.reason)
nana-request-changesThe owner sent it back with changes to make (detail.reason)

Slots

SlotDescription
(default)What is being asked for, in detail
attachmentsQuotes, photographs, invoices that support the request
actionsFurther controls beside the decision buttons

CSS parts

PartDescription
::part(base)The card
::part(header)The kind, heading and status row
::part(kind)The kind of request
::part(heading)The request's heading
::part(status)The status chip
::part(amount)The amount being asked for
::part(requester)Who asked, and when
::part(body)The request's detail
::part(attachments)The supporting documents
::part(actions)The decision buttons
::part(approve)The approve button
::part(reject)The reject button
::part(changes)The ask-for-changes button
::part(composer)The reason form
::part(reason)The reason field
::part(error)The message when a reason is missing
::part(decision)The record of the decision once made

CSS custom properties

PropertyDescription
--nana-approval-card-bgCard background
--nana-approval-card-borderCard border colour
--nana-approval-card-accentThe stripe that marks a request awaiting a decision
--nana-press-scaleHow far the buttons depress on press, shared with the other pressable controls
Explore Approval Card interactively Every state, prop, and edge case — with live controls — in Storybook.
View in Storybook