Powerful design with Priority & Intent
Using ‘priority’ and ‘intent’ to instill semantic meaning in design system variants.
In this article, we delve into a systematic and efficient approach for managing variants in a design system. By using two semantic metrics — “priority” and “intent” — we can bring consistency and intuitive design across all use cases.
Handling variants in a design system is a UI/UX challenge. Initially, a design system may use one component look for all needs. But as designers create more looks, this leads to an inconsistency that degrades user experience and maintenance.
These variants often arise on-the-fly from adapting a component to specific needs without much thought, causing missing user interactions, theming issues, and maintenance burden.
Initially, I grouped all variants in one folder, extracting shared code for uniformity. Yet, it still felt cluttered. Then, I realized these variants were all the same component, just with different “priority” and “intent”.
Adding these parameters enable controlled, consistent adaptation.
Here’s an example:
<Button>default button</Button>
<Button priority="cta">BUY NOW!</Button>
<Button priority="low" intent="danger">delete post</Button>
As you can see, the code expresses the buttons purpose rather than their appearance (form follows function, if you’d like). This simplifies theming with clearer design tokens, and it facilitates seamless design replacement:
// old design 'idustrial'
import { Button } from 'industrial-design';
// new design 'modern'
// seamlessly replaces 'modern'
// thanks to the common interface
// (html button + priority + intent)
//
// just find & replace
import { Button } from 'modern-design';
// remember to visually inspect!
<Button>default button</Button>
<Button priority="cta">BUY NOW!</Button>
<Button intent="danger" priority="low">delete post</Button>
Let’s explore this system’s implementation and advantages.
A word about “Primary” and “accent”
Before we proceed, we need to talk about the term “primary”.
The term “primary color” in design can be misleading, as it suggests it is the default, but in reality, it’s used more for emphasis. Instead, let’s use the term accent color, which truly represents its eye-catching role. This redefinition will be very useful in the implementation phase.
With that out of the way, let’s get started!
Priority
Priority communicates a component’s prominence, or its its capacity to demand attention and stands out. We can implement priority with various visual aspects, like accent colors, border, or a backdrop.
Using priority is as simple as:
<Button priority="low">low</Button>
<Button>medium</Button>
<Button priority="cta">Call To Action</Button>
High priority elements pave a path for your user, and therefor your application should have no more than one call to action at any give view. Default priority form the core of the user experience, while low-priority elements are ideal when you have a lot of small interactive options.
Deciding on the terms for these priorities posed a challenge. Simple terms like “low”, “medium”, and “high” are prone to misuse. Making “default” synonymous with “medium” and offering a short “low” and an explicit “CTA” seemed to strike a balanced middle ground.
Your project may call for different terms, and if that’s the case, I’d love to hear your ideas. Please share your thoughts in the comments below.
In implementing these variants, I utilized HTML data properties together with static CSS. This is a little easier to debug and has good performance, streamlining the design workflow. It looks like this:
// button.module.scss
.button {
//...
&[data-priority="low"] {
border: 2px solid transparent;
background: transparent;
color: var(--accent-color);
//...
}
&[data-priority="medium"] {
border: 2px solid var(--accent-color);
background: transparent;
color: var(--accent-color);
//...
}
&[data-priority="cta"] {
border: 2px solid var(--accent-color);
background: var(--background-accent);
color: var(--on-accent);
//...
}
}
export const ModernButton = ({ priority = "low", ...props }: Props) => (
<button data-priority={priority} className={styles.button} {...props} />
);
Intent
“Intent” communicates the semantic meaning of an element. Unlike Priority, which influences the prominence of an element, Intent controls its meaning, or rather, the emotion associated with it.
<Message intent="success">Operation Successful</Message>
<Message intent="danger">Something Went Wrong</Message>
Intents are most simply expressed through color, but they can extend to unique styles too. For instance, a “warning” intent might include an exclamation mark symbol.
Semantic naming is vital for intents. Using “warning” instead of “yellow” ensures a consistent interpretation of the component’s intent across various themes, independent of the actual color used.
For example, check out these components as they turn cyberpunk:
Here is an example implementation of intent:
[data-priority="low"] {
[data-intent="success"] {
color: var(--success-color);
}
}
[data-priority="medium"] {
[data-intent="danger"] {
color: var(--danger-color);
background: var(--danger-bg-color);
border-color: var(--danger-color);
}
}
[data-priority="cta"] {
[data-intent="warning"] {
color: var(--on-warning-color);
border-color: var(--warning-color);
background: var(--warning-bg-color);
}
}
// 6 more cases...
Does it seem repetitive? Yes, it does. But remember, this level of detail in CSS lets you fine-tune styles for each variant, and SASS scripting can do the heavy lifting (just ask ChatGPT!).
More interestingly, we can leverage CSS variables and the cascading nature of CSS for a more elegant solution — as we will see in the next section.
Cascadability
One of the remarkable aspects of this system is its ability to apply themes across entire component trees.
By leveraging the inherent cascading nature of CSS, we can establish a “micro-theme” of consistent aesthetics and functionality over composite components. A good example of cascading themes would be a dashboard, where different sections can adapt their look based on the overall system status. This mechanism streamlines the theming process, without having to write so much boilerplate.
// accents.module.css
.danger {
--accent-color: var(--danger-color);
--on-accent-color: var(--on-danger-color);
--accent-bg-color: var(--danger-bg-color);
}
// ...
// app.tsx
import accents from 'accents.module.css';
function Dashboard({ statuses }) {
return <div>
<StatusCard className={accents[statuses.all]} />
<StatusCard className={accents[statuses.deployment]} />
{/* ... */}
</divs>;
}
In this example, we pass the status prop to the Dashboard
component. It then applies the appropriate accent class to the StatusCard
, and all child components inherit the same style and adapt seamlessly.
In my experience, these cascading styles are enough to cover most cases, though you may still want the more granular approach from the previous section. For example, it can tweak form inputs, and icon-status.
Crafting Intents
Even though we often use intents like ‘default’, ‘neutral’, ‘success’, ‘warning’, and ‘danger’, you can still be creative.
I personally like using intents based on Magic: The Gathering or the Wu-xing philosophy. Their completeness make the intents more robust. For instance, I improved the terminology by using “danger” instead of “error”.
What I like about it is that every color carries a certain emotion and moral ideal, which helps communicating the intent to the user.
Remember that you can make every color either positive or negative, depending on the project style and design choices!
Retaining State
One of the approaches I tried was to create a distinct components for each variant, as a good for separation of concerns. Yet, given how React and the DOM function, this actually disrupts user experience!
For instance:
function SettingsPage() {
const [isModified, setModified] = useState(false);
return isModified ? <CTAButton>Save</CTAButton> : <Button>Save</Button>;
}
Toggling will cause React to unmount CTAButton
and mount Button
, breaking ongoing animations and state.
In contrast, this unified system might demand more in maintenance, but it gives us the critical benefit of keeping the state intact.
You can also arguably claim that it makes the code more readable:
function SettingsPage() {
const [isModified, setModified] = useState(false);
return <Button priority={isModified ? “cta” : undefined}>Save</Button>;
}
For maintenance, the key lies in a minimal well thought design. If your variants still get too complex, I suggest separating the styles into different files.
Interaction States
Designing interaction states is an integral part of component design, and it is essential to include them when creating variants.
You can read all about it in this article!
Conclusion
We delved into the benefit of ‘priority’ and ‘intent’ in design systems. ‘Priority’ controls visual prominence and efficiently guides user attention. ‘Intent’ adds meaning, enhancing semantics. Despite their obvious value, these principles are rare in design. I hope they’ll be widespread by the time you read this.
Naming can be tough, if you have some ideas, share your thoughts! Also, if you found this insight useful, please clap and subscribe for more.
Happy designing!