Agreement
betaConsent and codes of conduct: an "I agree" button that records who and when, never a checkbox. Once agreed, the record shows in its place with the person's photo.
Preview
stack
Code of conduct
Agreed by Anna Price ·
First aid consent
ForMia Price
Photo consent
ForMia Price
main
Terms of service
size
Code of conduct, sm
Code of conduct, md
Code of conduct, lg
states
Agreed
Agreed by Anna Price ·
Saving
Disabled
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/agreement
Use it for
- Codes of conduct, consents, and terms that someone must agree to, by name.
- Store the record in onAgree and pass it back as agreed: { person, photo, at }. The record showing in place is the confirmation, so no toast repeats it.
- A guardian agreeing for a junior passes forPerson. The record then says who it was for.
- A consent that can be taken back, like photo consent, passes onWithdraw.
- A required one left unagreed takes error, set when the form is sent. Its title and button edge turn red, its button glows once, and nothing changes size. Say what is missing on the button that was pressed, with shake() and new words, so it never moves.
- In a stack, each sits on the sheet with no box, divided by a faint line. Pass divider={false} on the first one. The sheet's own Continue is the main action, so the buttons stay outlines.
- When it is the surface's only decision, pass main, and the button is solid brand.
Avoid it for
- Settings that are just on or off. Use a switch.
- Picking among options. Use choice chips or a radio group.
- A checkbox for consent. A button that records who and when says more, and cannot be ticked by accident.
Props
- title
What is agreed to, like "Code of conduct". It names the agreement.
ReactNode
- text
The short version, in plain words.
ReactNode
- link
The full text: a link with href, or a button with onClick.
{ label: ReactNode; href?: string; onClick?: () => void }
- forPerson
Who it is for, when someone agrees on their behalf.
{ person: string; photo?: string }
- agreed
The record. Once set, it shows in place of the button: "Agreed by Anna Price · Tue 29 Sept, 18:04".
{ person: string; photo?: string; at?: string | Date } | null
- onAgree
Store the record here, then pass it back as agreed.
() => void
- onWithdraw
Shows Withdraw on the record. Clear agreed here.
() => void
- error
For a required one left unagreed. The title and button edge turn red and the button glows once, with no change in size. The words describe the button for screen readers.
ReactNode
- divider
A faint line above it that fades out at both ends. Pass false on the first of a stack.
boolean · default true
- agreeLabel
The button's words.
ReactNode · default "I agree"
- main
The surface's only decision: the button is solid brand. Off, it is an outline.
boolean · default false
- size
Text, button, and photo size.
"sm" | "md" | "lg" · default "md"
- loading
The button shows a spinner while the record is saved.
boolean · default false
- disabled
Blocks the button and Withdraw.
boolean · default false
- locale
Formats the record's date and time.
string · default "en-GB"
- timeZone
The zone the record's time shows in, like "Europe/London". By default, wherever it renders. Pass one when a server renders the page.
string
- className
Classes for the root.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | ReactNode | - | What is agreed to, like "Code of conduct". It names the agreement. |
| text | ReactNode | - | The short version, in plain words. |
| link | { label: ReactNode; href?: string; onClick?: () => void } | - | The full text: a link with href, or a button with onClick. |
| forPerson | { person: string; photo?: string } | - | Who it is for, when someone agrees on their behalf. |
| agreed | { person: string; photo?: string; at?: string | Date } | null | - | The record. Once set, it shows in place of the button: "Agreed by Anna Price · Tue 29 Sept, 18:04". |
| onAgree | () => void | - | Store the record here, then pass it back as agreed. |
| onWithdraw | () => void | - | Shows Withdraw on the record. Clear agreed here. |
| error | ReactNode | - | For a required one left unagreed. The title and button edge turn red and the button glows once, with no change in size. The words describe the button for screen readers. |
| divider | boolean | true | A faint line above it that fades out at both ends. Pass false on the first of a stack. |
| agreeLabel | ReactNode | "I agree" | The button's words. |
| main | boolean | false | The surface's only decision: the button is solid brand. Off, it is an outline. |
| size | "sm" | "md" | "lg" | "md" | Text, button, and photo size. |
| loading | boolean | false | The button shows a spinner while the record is saved. |
| disabled | boolean | false | Blocks the button and Withdraw. |
| locale | string | "en-GB" | Formats the record's date and time. |
| timeZone | string | - | The zone the record's time shows in, like "Europe/London". By default, wherever it renders. Pass one when a server renders the page. |
| className | string | - | Classes for the root. |
Accessibility
- The root is a group named by the title, so each "I agree" in a stack is heard with what it agrees to.
- An error is described on the button. It is not announced here: the press that found it says it out loud, like a role="alert" beside the button that was pressed.
- The glow follows the --motion-* tokens, so reduced motion skips it. The red title and edge stay.
- Once agreed, focus moves from the button to the record, so screen readers read who agreed and when. After Withdraw it moves back to the button.
- The record's date is a <time> element with a machine-readable dateTime.
- The faint divider is hidden from screen readers. In forced-colors mode it draws as a plain border.
Examples
In a stack
<div className="flex flex-col gap-4">
<Agreement
divider={false}
title="Code of conduct"
text="Treat everyone with respect, keep shared spaces tidy, and report problems early."
link={{ label: "Read the code of conduct", href: "/conduct" }}
agreed={{ person: "Anna Price", at: "2026-09-29T18:04" }}
/>
<Agreement
title="Photo consent"
forPerson={{ person: "Mia Price" }}
error="The club needs photo consent to share team photos."
/>
</div>The only decision
<Agreement main title="Terms of service" text="Invoices are due on the first day of each month." />