.. _developers_ui_components:
======================
Creating UI components
======================
UI components are (reusable) logical building blocks to build user interfaces. Some
obvious examples are buttons and links. More complex examples are footers with navigation
elements or a form field input with label, input and validation errors.
Development of (new) UI components in Open Forms is subject to some guidelines and
rules, with the goal of:
* contribute to good accessibility
* make it possible to *theme* them, so appearance can be tailored to the organization
* being reusable in similar but different contexts
* make it easy to maintain the code
The next sections describe how to approach creating or modifying UI components.
Check for existing community components
=======================================
The `NL Design System components `_
catalogue already lists a large number of components that may fit your requirements.
You should always check first if there's anything suitable. This can lead to an easily
re-used community component, or at a minimum provide resources about how to implement
such a component.
Make sure to read up on how other organizations use components, which design constraints
they have taken into consideration and where the variation lies if different community
implementations exist.
If nothing exists that is readily usable, you can proceed with implementing it yourself,
but you should still keep in mind the NL DS research so that future implementations can
converge on a single approach. You can explore whether the NL DS community is open to
contributions for such a component if you want too.
Building a component from scratch
=================================
Composition
-----------
Often you need to build a higher level component that's composed of other lower level
components. That's normal - the obvious low-level components like buttons and links are
seldomly used in isolation, and we encourage re-using these instead of reinventing the
wheel.
Consider the footer example:
.. code-block:: html
This simple example captures the essence well: the appearance of an individual link
should be consistent with other links on the page, so you re-use the ``utrecht-link``
component.
However, the footer gets an application-specific class name, so that the layout and
appearance (such as flex display with left or center alignment, background color...)
can be specified on that element.
Markup
------
Use semantic HTML, and try to use only what's necessary. Avoid wrapper ``div`` and
``span`` elements if you can - often with Flexbox and CSS Grid styling you can get quite
far for layout management.
If you're not yet familiar with the implicit accessible roles of elements, take some
time to do the research. E.g. it may seem obvious to use a ``nav`` element, but if this
results in multiple such elements on a page, additional work is needed to give them
distinctive accessible labels.
The question "How would this appear to a screenreader" should always be at the forefront
when writing HTML.
Block-Element-Modifier (BEM)
----------------------------
`BEM `_ is a methodology for reusable components.
**Blocks**
For the CSS and CSS class names, the BEM methodology is used. In a nutshell, this means
the outer element gets a ``.my-block`` classname that encapsulates the component and
is the outer CSS target selector.
Example:
.. code-block:: html
.. tip::
* You should try to limit the number of different blocks that exist in a codebase.
* Avoid using extremely specific blocks that cannot be reused in other places.
**Elements**
The element can contain zero or more child elements, which get a ``.my-block__element-1``
or ``.my-block__element-2`` class name. They can exist anywhere in the HTML tree, they
are not limited to direct children - see the example below.
A child element cannot exist / does not have meaning without its block parent.
For example:
.. code-block:: html
Note that in HTML ``openforms-footer__item`` is a child element of
``openforms-footer__group``, but from the BEM names and styling you cannot infer this
relationship.
.. warning::
You can have only **one** occurrence of the `__` separator. A class name like
``.block__element__nested`` is not valid according to BEM, and a red flag that
your class names are mimicking your HTML structure.
.. tip::
It's okay to combine an element and block class name, e.g.
``utrecht-link openforms-footer__link`` can make sense. The styles from the element
are then scoped to when a link is a child of the block element.
**Modifiers**
Modifiers indicate variants of a block and/or element. The double dash (``--``)
separator is used: ``.block--modifier`` and ``.block__element--modifier``.
Modifiers can be applied to both blocks and elements, e.g.:
.. code-block:: html
.. warning::
A modifier is always relative to its base block or element, meaning you must include
that base class name too. ``...`` is
incorrect usage, instead it must be:
``...``
**SCSS helpers**
Most of our repositories have helpers to build the correct BEM class names:
* ``formio-renderer`` -> use ``@/scss/bem``
* ``formio-builder`` -> no helper available
* remainder -> use ``microscope-sass/lib/bem``, but keep in mind that ``microscope-sass``
is deprecated and being phased out.
Example usage:
.. code-block:: scss
@use '@/scss/bem';
.openforms-footer {
display: flex;
flex-direction: row;
@include bem.modifier('vertical') {
// modifier styles
flex-direction: column;
}
@include bem.element('group') {
// group styles
list-style: none;
margin: 0;
padding: 0;
@include bem.modifier('compact') {
// group modifier styles
}
}
@include bem.element('item') {
// item styles
}
}
CSS rules
---------
**BEM and spacing**
Blocks should ideally never define margins. For spacing control, you should use paddings
instead. A block is the atomic unit and is synonym to "component". Defining margins
messes with the layout when a block is used in a larger UI and can lead to unintended
wrapping.
Elements don't have this restricting, because they are contained within their block and
not expected to leak outside their container.
**Design tokens and CSS variables**
Theming support is implemented using CSS custom properties (a.k.a. variables). Certain
appearance properties may be relevant for custom themes, such as colors, spacing, font
family and font-style.
Design tokens are a mechanism to provide values for these styles - design tokens are
typically defined in JSON, which compiles down to (amongst other formats) CSS.
An example:
.. code-block:: json
{
"of": {
"footer": {
"background-color": {"value": "transparent"},
"color": {"value": "#000000"},
"group": {
"compact": {
"padding-block-start": {"value": "4px"}
}
}
}
}
}
Gets compiled to:
.. code-block:: css
.openforms-theme {
--of-footer-background-color: transparent;
--of-footer-color: #000000;
--of-footer-group-compact-padding-block-start: 4px;
}
Each component can only consume design tokens that it "owns" - you may not consume
other tokens that happen or are likely to have the same value:
.. code-block:: scss
:caption: Valid example
.openforms-footer {
background-color: var(--of-footer-background-color);
color: var(--of-footer-color);
}
.. code-block:: scss
:caption: Invalid example
.openforms-footer {
background-color: var(--of-color-primary);
color: var(--of-color-text);
}
This minimizes the dependencies on other components/tokens structure.
Theme builders that create the design tokens can opt to use these references to common
or brand tokens, but this also leaves the flexibility to fine tune the exact appearance
of each component.
**Design tokens and BEM**
Design tokens and BEM work well together, because they create a natural hierarchy. If
you have a ``component`` (or, in BEM-terms, a block), this is the name after the ``of``
namespace. Modifiers and elements are additional nodes.
For example, you can have the following token name patterns:
* ``of.component.property``
* ``of.component.block-modifier.property``
* ``of.component.element.property``
* ``of.component.element.element-modifier.property``
* ``of.component.block-modifier.element.property``
* ``of.component.block-modifier.element.element-modifier.property``
* ``of.component.block-modifier.ui-state.property``
* etc.
where ``property`` is a (CSS) property like ``color`` or ``padding-inline-start``,
and ``ui-state`` can be states like ``hover``, ``focus``...
**Use logical properties**
To be prepared for other text directions than left-to-right and top-to-bottom, use the
logical properties for styling instead of their literal orientations.
As a quick example:
* Use ``margin-block-start`` instead of ``margin-top``.
* Use ``margin-block-end`` instead of ``margin-bottom``.
* Use ``margin-inline-start`` instead of ``margin-left``.
* Use ``margin-inline-end`` instead of ``margin-right``.
Updating existing code
======================
The Open Forms codebase started out from a prototype, and over the past 3 or more years,
we have gained a lot more insights and experience. On top of that, technology keeps
evolving and newer browser features may be available now that weren't at the time.
So, when updating existing code, whether that's refactoring or making enhancements,
take a critical look at the existing code and the surrounding code. Especially if you
see the last change to that code happened 2+ years ago!
Ask yourself the following questions, and do the necessary research if you don't know
the answers:
* Is all of this markup/CSS necessary? Can we slim it down?
* How is this experienced by screenreader users? Is the accessibility correct?
* Do we need custom markup/CSS, or can we refactor this by using existing NL DS components?
* Are there themeable aspects that currently don't have design tokens? Is the design token
usage correct?
* If I make changes, can I do this in a backwards compatible way?