--- title: 3.10. UI Guide desc: Learn UI development with React & Rumext, design system implementation, and performance considerations. See Penpot's technical guide. Free to use! --- # 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. - 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* {: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* {:icon-id icon :size "m" :class (stl/css :icon)}]) [: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); } ``` Note: Thanks to CSS Modules, identical class names defined in different files are scoped locally and do not cause naming collisions. ### Use CSS logical properties The [logical properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_logical_properties_and_values) define styles relative to the content’s writing mode (e.g., inline, block) instead of physical directions (left, right, etc). This improves support for right-to-left (RTL) languages and enhances layout flexibility. ❌ **AVOID: Physical properties** ```scss .btn { padding-left: var(--sp-xs); } ``` ✅ **DO: Use direction‐relative equivalents** ```scss .btn { padding-inline-start: var(--sp-xs); } ``` Note: Although `width` and `height` are physical properties, their use is allowed in CSS files. They remain more readable and intuitive than their logical counterparts (`inline-size`, `block-size`) in many contexts. Since our layouts are not vertically-sensitive, we don't gain practical benefits from using logical properties here. ### Use named DS variables Avoid hardcoded values like `px`, `rem`, or raw SASS variables `($s-*)`. Use semantic, named variables provided by the Design System to ensure consistency and scalability. #### Spacing (margins, paddings, gaps...) Use variables from `frontend/src/app/main/ui/ds/spacing.scss`. These are predefined and approved by the design team — **do not add or modify values without design approval**. #### Fixed dimensions For fixed dimensions (e.g., modals' widths) defined by design and not layout-driven, use or define variables in `frontend/src/app/main/ui/ds/_sizes.scss`. To use them: ```scss @use "../_sizes.scss" as *; ``` Note: Since these values haven't been semantically defined yet, we’re temporarily using SASS variables instead of named CSS custom properties. #### Border Widths Use border thickness variables from `frontend/src/app/main/ui/ds/_borders.scss`. To import: ```scss @use "../_borders.scss" as *; ``` Avoid using sass variables defined on `frontend/resources/styles/common/refactor/spacing.scss` that are deprecated. ❌ **AVOID: Using sass unnamed variables or hardcoded values** ```scss .btn { padding: $s-24; } .icon { width: 16px; } ``` ✅ **DO: Use DS variables** ```scss .btn { padding: var(--sp-xl); } .icon { width: var(--sp-l); } ``` ### Use Proper Typography Components Replace plain text tags with `text*` or `heading*` components from the Design System to ensure visual consistency and accessibility. ❌ **AVOID: Using text wrappers** ```clojure [:h2 {:class (stl/css :modal-title)} title] [:div {:class (stl/css :modal-content)} "Content"] ``` ✅ **DO: Use spacing named variables** ```clojure ... [app.main.ui.ds.foundations.typography :as t] [app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.text :refer [text*]] ... [:> heading* {:level 2 :typography t/headline-medium :class (stl/css :modal-title)} title] [:> text* {:as "div" :typography t/body-medium :class (stl/css :modal-content)} "Content"] ``` When applying typography in SCSS, use the proper mixin from the Design System. ❌ **AVOID: Deprecated mixins** ```scss .class { @include headlineLargeTypography; } ``` ✅ **DO: Use the DS mixin** ```scss @use "../ds/typography.scss" as t; .class { @include t.use-typography("body-small"); } ``` You can find the full list of available typography tokens in [Storybook](https://design.penpot.app/storybook/?path=/docs/foundations-typography--docs). If the design you are implementing doesn't match any of them, ask a designer. ### Use custom properties within components Reduce the need for one-off SASS variables by leveraging [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties) in your component styles. This keeps component theming flexible and composable. For instance, this is how we handle the styles of \, which have a different style depending on the level of the message (default, info, error, etc.) ```scss .toast { // common styles for all toasts // ... --toast-bg-color: var(--color-background-primary); --toast-icon-color: var(--color-foreground-secondary); // ... more variables here background-color: var(--toast-bg-color); } .toast-icon { color: var(--toast-bg-color); } .toast-info { --toast-bg-color: var(--color-background-info); --toast-icon-color: var(--color-accent-info); // ... override more variables here } .toast-error { --toast-bg-color: var(--color-background-error); --toast-icon-color: var(--color-accent-error); // ... override more variables here } // ... more variants here ``` ## Semantics and accessibility All UI code must be accessible. Ensure that your components are designed to be usable by people with a wide range of abilities and disabilities. ### Let the browser do the heavy lifting When developing UI components in Penpot, we believe it is crucial to ensure that our frontend code is semantic and follows HTML conventions. Semantic HTML helps improve the readability and accessibility of Penpot. Use appropriate HTML tags to define the structure and purpose of your content. This not only enhances the user experience but also ensures better accessibility. 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 \