sapui5tutors SAPUI5 • Fiori • SAP BTP Step-by-step tutorials Real project examples Interview Q&A
Practical SAPUI5 • Fiori • SAP BTP tutorials and interview prep

SAPUI5 Custom Controls: Build Your First One

Standard controls cover most screens, but sometimes you need something the library doesn't offer — say you have a rating widget with a custom animation, or a tag cloud with domain-specific behavior. That's when you build a custom control. It's less intimidating than it sounds: extend a base control, define a renderer, add properties. Let's build one.

The anatomy of a custom control

A custom control has two parts: the control class (API, properties, events) and the renderer (HTML output). Keep them separate — logic in the control, markup in the renderer.

sap.ui.define([
  "sap/ui/core/Control"
], (Control) => {
  "use strict";
  return Control.extend("my.app.control.StarRating", {
    metadata: {
      properties: {
        value: { type: "int", defaultValue: 0 },
        max:   { type: "int", defaultValue: 5 }
      },
      events: {
        change: { parameters: { value: { type: "int" } } }
      }
    },
    setValue: function (iValue) {
      this.setProperty("value", iValue, true); // suppress re-render
      this.fireChange({ value: iValue });
      return this;
    },
    renderer: {
      apiVersion: 2,
      render: function (oRm, oControl) {
        oRm.openStart("div", oControl)
           .class("starRating")
           .openEnd();
        for (let i = 1; i <= oControl.getMax(); i++) {
          oRm.openStart("span")
             .class(i <= oControl.getValue() ? "star on" : "star")
             .attr("data-value", i)
             .openEnd()
             .text("★")
             .close("span");
        }
        oRm.close("div");
      }
    }
  });
});

Notice apiVersion: 2 — the modern renderer API. The old string-concatenation style is deprecated; use the semantic rendering methods.


Wiring up interaction

Rendering is half the job. Attach DOM events in onAfterRendering so the stars are clickable.

onAfterRendering: function () {
  this.$().find(".star").on("click", (oEvent) => {
    this.setValue(parseInt(oEvent.currentTarget.dataset.value, 10));
  });
},

Because setValue fires the change event, parent views can bind to it like any standard control event. Your custom control now behaves as a first-class citizen.

Always fire events for state changes instead of expecting parents to poll. Consumers of your control should never need to know its internals.


Using it in a view

Reference your control's namespace in the XML view and use it like a built-in control, including data binding.

<mvc:View xmlns:custom="my.app.control" ...>
  <custom:StarRating
    value="{product>Rating}"
    max="5"
    change=".onRatingChange"/>
</mvc:View>

Binding works automatically because the property follows the standard getter/setter conventions from the metadata.


Before you build: check twice

  • Composite instead: if the widget is just a combination of existing controls (a Label plus an Input), build a composite — an XML fragment or a control that aggregates others — rather than a renderer from scratch.
  • Extend, don't reinvent: need a special table? Extend sap.m.Table instead of rebuilding one.
  • Style with CSS: keep all visuals in a stylesheet. The renderer should emit semantic markup and classes, not inline styles.

Build custom controls for genuinely new behavior, not for styling. Nine times out of ten, CSS plus a composite gets you there with far less code.

What custom control are you planning to build? Describe it in the comments.