--- title: 3.9. UI Guide --- # UI Guide These are the guidelines for developing UI in Penpot, including the design system. ## React & Rumext The UI in Penpot uses React v18 , with the help of [rumext](https://github.com/funcool/rumext) for providing Clojure bindings. See [Rumext's User Guide](https://funcool.github.io/rumext/latest/user-guide.html) to learn how to create React components with Clojure. ## General guidelines We want to hold our UI code to the same quality standards of the rest of the codebase. In practice, this means: - UI components should be easy to maintain over time, especially because our design system is ever-changing. - UI components should be accessible, and use the relevant HTML elements and/or Aria roles when applicable. - We need to apply the rules for good software design: - The code should adhere to common patterns. - UI components should offer an ergonomic "API" (i.e. props). - UI components should favor composability. - Try to have loose coupling. ### Composability **Composability is a common pattern** in the Web. We can see it in the standard HTML elements, which are made to be nested one inside another to craft more complex content. Standard Web components also offer slots to make composability more flexible. Our UI components must be composable. In React, this is achieved via the children prop, in addition to pass slotted components via regular props. #### Use of children > **⚠️ NOTE**: Avoid manipulating children in your component. See [React docs](https://react.dev/reference/react/Children#alternatives) about the topic. ✅ **DO: Use children when we need to enable composing** ```clojure (mf/defc primary-button* {::mf/props :obj} [{:keys [children] :rest props}] [:> "button" props children]) ``` ❓**Why?** By using children, we are signaling the users of the component that they can put things _inside_, vs a regular prop that only works with text, etc. For example, it’s obvious that we can do things like this: ```clojure [:> button* {} [:* "Subscribe for " [:& money-amount {:currency "EUR" amount: 3000}]]] ``` #### Use of slotted props When we need to either: - Inject multiple (and separate) groups of elements. - Manipulate the provided components to add, remove, filter them, etc. Instead of children, we can pass the component(s) via a regular a prop. #### When _not_ to pass a component via a prop It's about **ownership**. By allowing the passing of a full component, the responsibility of styling and handling the events of that component belong to whoever instantiated that component and passed it to another one. For instance, here the user would be in total control of the icon component for styling (and for choosing which component to use as an icon, be it another React component, or a plain SVG, etc.) ```clojure (mf/defc button* {::mf/props :obj} [{:keys [icon children] :rest props}] [:> "button" props icon children]) ``` However, we might want to control the aspect of the icons, or limit which icons are available for this component, or choose which specific React component should be used. In this case, instead of passing the component via a prop, we'd want to provide the data we need for the icon component to be instantiated: ```clojure (mf/defc button* {::mf/props :obj} [{:keys [icon children] :rest props}] (assert (or (nil? icon) (contains? valid-icon-list icon) "expected valid icon id")) [:> "button" props (when icon [:> icon* {:id icon :size "m"}]) children]) ``` ### Our components should have a clear responsibility It's important we are aware of: - What are the **boundaries** of our component (i.e. what it can and cannot do) - Like in regular programming, it's good to keep all the inner elements at the same level of abstraction. - If a component grows too big, we can split it in several ones. Note that we can mark components as private with the ::mf/private true meta tag. - Which component is **responsible for what**. As a rule of thumb: - Components own the stuff they instantiate themselves. - Slotted components or children belong to the place they have been instantiated. This ownership materializes in other areas, like **styles**. For instance, parent components are usually reponsible for placing their children into a layout. Or, as mentioned earlier, we should avoid manipulating the styles of a component we don't have ownership over. ## Styling components We use **CSS modules** and **Sass** to style components. Use the (stl/css) and (stl/css-case) functions to generate the class names for the CSS modules. ### Allow passing a class name Our components should allow some customization by whoever is instantiating them. This is useful for positioning elements in a layout, providing CSS properties, etc. This is achieved by accepting a class prop (equivalent to className in JSX). Then, we need to join the class name we have received as a prop with our own class name for CSS modules. ```clojure (mf/defc button* {::mf/props :obj} [{:keys [children class] :rest props}] (let [class (dm/str class " " (stl/css :primary-button)) props (mf/spread-props props {:class class})] [:> "button" props children])) ``` ### About nested selectors Nested styles for DOM elements that are not instantiated by our component should be avoided. Otherwise, we would be leaking CSS out of the component scope, which can lead to hard-to-maintain code. ❌ **AVOID: Styling elements that don’t belong to the component** ```clojure (mf/defc button* {::mf/props :obj} [{:keys [children] :rest props}] (let [props (mf/spread-props props {:class (stl/css :primary-button)})] ;; note that we are NOT instantiating a here. [:> "button" props children])) ;; later in code [:> button* {} [:> icon {:id "foo"}] "Lorem ipsum"] ``` ```scss .button { // ... svg { fill: var(--icon-color); } } ``` ✅ **DO: Take ownership of instantiating the component we need to style** ```clojure (mf/defc button* {::mf/props :obj} [{:keys [icon children class] :rest props}] (let [props (mf/spread-props props {:class (stl/css :button)})] [:> "button" props (when icon [:> icon* {:id icon :size "m"}]) [:span {:class (stl/css :label-wrapper)} children]])) ;; later in code [:> button* {:icon "foo"} "Lorem ipsum"] ``` ```scss .button { // ... } .icon { fill: var(--icon-color); } ``` ### Favor lower specificity This helps with maintanibility, since lower [specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity) styles are easier to override. Remember that nesting selector increases specificity, and it's usually not needed. However, pseudo-classes and pseudo-elements don't. ❌ **AVOID: Using a not-needed high specificity** ```scss .btn { // ... .icon { fill: var(--icon-color); } } ``` ✅ **DO: Choose selectors with low specificity** ```scss .btn { // ... } .icon { fill: var(--icon-color); } ``` ## Accessibility ### Let the browser do the heavy lifting Whenever possible, leverage HTML semantic elements, which have been implemented by browsers and are accessible out of the box. This includes: - Using \ for link (navigation, downloading files, sending e-mails via mailto:, etc.) - Using \