Skip to content
ONIWC
iwc 0.1.0

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.iwc

Step 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" }
    ]
  }
}
PartMeaning
propsThe component's inputs. Each is a field spec: type, required, enum, default, description
rootThe 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 test
Output
✓ 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 failed

The 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 child elements there. 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

MessageFix
props.label: props only exist inside componentsprops. works only inside a component's root.
component elements need "use"Name the component file.
circular component dependencyA component ended up using itself.

Next: Create a reusable layout.

Source: apps/marketing/content/docs/tutorials/reusable-button.md