Tutorial 3: Create a reusable button
Objective: build a call-to-action button once, as a component with props, and use it with different labels, links and tones.
Prerequisites: Tutorial 2. Create a fresh project (iwc create buttons --yes) or continue in your own.
The finished project:
Project files
buttons/
├── components/
│ └── cta-button.component.iwc
├── styles/
│ └── main.css
├── tests/
│ └── buttons.test.iwc
├── ui/
│ ├── home.ui.iwc
│ ├── pricing.ui.iwc
│ └── signup.ui.iwc
├── app.iwc
└── package.iwcStep 1: Define the component
Create components/cta-button.component.iwc:
components/cta-button.component.iwc
{
"name": "CtaButton",
"props": {
"label": { "type": "string", "required": true, "description": "the text on the button" },
"href": { "type": "string", "required": true, "description": "where it goes: a route, #anchor or URL" },
"tone": { "type": "string", "enum": ["solid", "outline", "quiet"], "default": "solid" }
},
"root": {
"type": "link",
"target": "=props.href",
"className": "=concat('cta cta-', props.tone)",
"elements": [
{ "type": "text", "variant": "span", "bind": "props.label" },
{ "type": "icon", "name": "arrow-right" }
]
}
}| Part | Meaning |
|---|---|
props | The component's inputs. Each is a field spec: type, required, enum, default, description |
root | The single element the component renders. Here a link containing a text and an icon |
"bind": "props.label" | Shows the label prop |
"target": "=props.href" | An expression: the link goes wherever href says |
"className": "=concat('cta cta-', props.tone)" | Builds the class cta cta-solid (or -outline, -quiet) from the tone |
tone has a default, so callers may leave it out; label and href are required, so leaving them out is a compile error.
Step 2: Use it
ui/home.ui.iwc
{
"name": "Home",
"route": "/",
"title": "Reusable buttons",
"sections": [
{
"type": "section",
"className": "page",
"layout": { "direction": "column", "gap": "24px" },
"elements": [
{ "type": "text", "variant": "h1", "content": "One button, three tones" },
{
"type": "row",
"layout": { "gap": "12px", "wrap": true },
"elements": [
{ "type": "component", "use": "cta-button.component.iwc", "props": { "label": "Start free trial", "href": "/signup" } },
{ "type": "component", "use": "cta-button.component.iwc", "props": { "label": "See pricing", "href": "/pricing", "tone": "outline" } },
{ "type": "component", "use": "cta-button.component.iwc", "props": { "label": "Read the docs", "href": "#docs", "tone": "quiet" } }
]
}
]
}
]
}A component element names the file in use and passes props. The compiler checks every use:
cta-button.component.iwc requires prop "href"— a required prop is missing;cta-button.component.iwc has no prop "colour"— a misspelled or undeclared prop.
Step 3: Style it
styles/main.css
.page { max-width: 880px; margin: 0 auto; padding: 64px 16px; }
/* The button component: one base class, one class per tone. */
.cta {
display: inline-flex; align-items: center; gap: 8px;
padding: 12px 20px; border-radius: var(--radius);
font-weight: 600; text-decoration: none;
border: 2px solid var(--color-primary);
transition: transform 150ms ease, box-shadow 150ms ease;
}
.cta .iwc-icon { width: 18px; height: 18px; }
.cta:hover { transform: translateY(-1px); box-shadow: 0 8px 20px rgba(91, 76, 245, 0.25); }
.cta-solid { background: var(--color-primary); color: var(--color-on-primary); }
.cta-outline { background: transparent; color: var(--color-primary); }
.cta-quiet { border-color: transparent; color: var(--color-text); }and register the stylesheet in app.iwc:
app.iwc
{
"name": "Buttons",
"iwc": 1,
"theme": {
"tokens": {
"color-primary": "#5b4cf5",
"color-on-primary": "#ffffff",
"color-text": "#14141f",
"color-border": "#e6e6ef",
"radius": "12px",
"font-body": "system-ui, sans-serif"
}
},
"styles": ["styles/main.css"]
}Step 4: Test it
tests/buttons.test.iwc
{
"name": "buttons",
"cases": [
{
"name": "each instance renders its own label, link and tone",
"request": { "path": "/" },
"expect": {
"status": 200,
"contains": ["Start free trial", "href=\"/signup\"", "cta-solid", "See pricing", "href=\"/pricing\"", "cta-outline", "cta-quiet"]
}
}
]
}Terminal
iwc check && iwc testOutput
✓ targets: web
✓ 7 files · 3 screens · 0 layouts · 1 components · 0 apis · 0 schemas · 0 actions
✓ buttons › each instance renders its own label, link and tone
1 passed, 0 failedThe test shows the first button got cta-solid without passing tone: the default applied.
Going further
- Slots: a component with a
{"type": "slot"}element renders the caller's own childelementsthere. See Components & slots. - From data: inside a list, pass a record field with
"props": {"label": {"bind": "name"}}. - Button actions: for a button that does something other than navigate (open a dialog, call an API), use the button element as the root.
Troubleshooting
| Message | Fix |
|---|---|
props.label: props only exist inside components | props. works only inside a component's root. |
component elements need "use" | Name the component file. |
circular component dependency | A component ended up using itself. |
Next: Create a reusable layout.
Source: apps/marketing/content/docs/tutorials/reusable-button.md