If I Was Starting My Career Today: Thoughts After 15 Years Spent In UX Design (Part 1)

Category Image 062

My design career began in 2008. The first book that I read on the topic of design was Photoshop Tips And Tricks by Scott Kelby, which was a book about a very popular design tool, but not about user experience (UX) design itself. Back at the time, I didn’t know many of the approaches and techniques that even junior designers know today because they weren’t invented yet, and also because I was just beginning my learning journey and finding my way in UX design. But now I have diverse experience; I’m myself hiring designers for my team, and I know much more.

In my two-part series of articles, I’ll try to share with you what I wish I knew if I was starting my career today.

“If you want to go somewhere, it is best to find someone who has already been there.”

Robert Kiyosaki

The two-part series contains four sections, each roughly covering one key stage in your beginner career:

  1. Master Your Design Tools
  2. Work on Your Portfolio
  3. Preparing for Your First Interviews: Getting a First Job
  4. In Your New Junior UX Job: On the Way to Grow

I’ll cover the first three topics in this first article and the fourth one in the second article. In addition, I will include very detailed Further Reading sections at the end of each part.

When you’re about to start learning, every day, you will receive new pieces of evidence of how many things you don’t know yet. You will see people who have been doing this for years and you will doubt whether you can do this, too. But there is a nuance I want to highlight: first, take a look at the following screenshot:

This is the Amazon website in 2008 when I was about to start my design career and received my first paycheck as a beginner designer.

And this is how Amazon looked like even earlier, in 2002:

Source: versionmuseum.com. (Large preview)

In 2002, Amazon made 3.93 billion US dollars in profits. I dare say they could have hired the very best designers at the time. So today, when you speak to a designer with twenty years of experience and think, “Oh, this designer must be on a very high level now, a true master of his craft,” remind yourself about the state of UX design that existed when the designer’s career was about to start, sometime in the early 2000s!

A lot of the knowledge that I have learned and that is over five years old is outdated now, and the learning complexity only increases every year.

It doesn’t matter how many years you have been in this profession; what matters are the challenges you met in the last few years and the lessons you’ve learned from them.

Are you a beginner or an aspiring user interface/user experience designer? Don’t be afraid to go through all the steps in your UX design journey. Patience and a good plan will let you become a good designer faster than you think.

“The best time to start was yesterday. The next best time is now.”

H. Jackson Brown, Jr.

This was the more philosophical part of my writing, where I wanted to help you become better motivated. Now, let’s continue with the more practical things and advice!

Getting Started: Master Your Design Tools

When I was just beginning to learn, most of us did our design work in Adobe Photoshop.

In Photoshop, there were no components, styles, design libraries, auto layouts, and so on. Every screen was in another PSD file, and even making rounded corners on a rectangle object was a difficult task. Files were “heavy,” and sometimes I needed to wait thirty or more seconds to open a file and check what screen was inside while changing a button’s name or label in twenty separate PSD files (each containing only one design screen, remember?) could take up to an hour, depending on the power of your computer.

There were many digital design tools at the time, including Fireworks — which some professionals considered superior to Photoshop, and for quite a few reasons — but this is not the main point of my story. One way or another, Photoshop back then became very popular among designers and we all absolutely had to have it in our toolset, no matter what other tools we also needed and used.

Now computers are much faster, and our design tools have evolved quite a bit, too. For example, I can apply multiple changes to multiple design screens in just a few seconds by using Figma components and a proper structure of the design file, I can design/prototype responsive designs by using auto-layout, and more.

In one word, knowing your design tool can be a real “superpower” for junior UX designers — a power that beginners often ignore. When you know your tool inside-out, you’ll spend less time on the design routine and you’ll have more time for learning new things.

Master your tool(s) of choice (be it Figma Design or Illustrator, Sketch, Affinity Designer, Canva, Framer, and so on) in the most efficient way, and free up to a couple of extra hours every day for reading, doing tutorials, or taking longer breaks.

Learn all the key features and options, and discover and remember the most important hotkeys so you’ll be working without the need to constantly reach for your mouse and navigate the “web” of menus and sub-menus. It’s no secret that we, designers, mostly learn through doing practical tasks. So, imagine how much time it would save you within a few years of your career!

Also, it’s your chance: developers are rolling out new features for beginner designers and pro designers simultaneously, but junior designers usually have more time to learn those updates! So, be faster and get your advantage!

Getting Started: Work On Your Portfolio

You need to admit it: your portfolio (or, to put it more precisely, the lack of it) will be the main pain point at the start.

You may hear sometimes statements such as: “We understand that being a junior designer is not about having a portfolio...” But the fact is that we all would like to see some results of your work, even if it is your very early work on a few design projects or concepts. Remember, if you have something to show, this would always be a considerable advantage!

I have heard from some juniors that they don’t want to invest time in their portfolio because this work is not payable and it’s time-consuming. But sitting and waiting and getting rejected again and again is also time-consuming. And spending a few of your first career years in the wrong company is also time-consuming (and disappointing, too). So my advice is to spend some time in advance on showcasing your work and then get much better results in the near future.

In case you need some extra motivation, here is a quote from Muhammad Ali, regarded as one of the most significant sports figures of the 20th century:

“I hated every minute of training, but I said to myself, ‘Do not quit. Suffer now and live the rest of your life as a champion.’”

— Muhammad Ali

Ready to fire but have no idea where to start? Here are a few options:

  • Find a popular product with a rather difficult-to-use or not very elegant interface and research what the users of this product are complaining about the most. Then, as an exercise, design a few interface screens for this product, with their core features explained, publish them on social media, and tag that company. (This approach may not always work, but it’s worth a try.)
  • Sign up for and actively participate in hackathons. As a result, it’s possible that you may get not just a few screens redesigned in Figma but a real working product you can show (and be proud of). Also, you can meet nice people there who may recommend you if you apply for a job at one of the companies they work for.
  • Complete UXchallenge challenges and present how you solved them on LinkedIn.
    Note: You’re not limited to LinkedIn, of course; you can also use Instagram, Facebook, Behance, Dribbble, and so on. But keep in mind that many recruiters prefer LinkedIn.
  • Pick up a website that you use often and check whether it meets the “Ten Usability Heuristics for User Interface Design.” Create a detailed report that lists everything that can be (re)designed better. Publish the report on LinkedIn and also send it to the company that made this website. Don’t forget to tell them why you did that report for their website specifically and that you’re learning UX design, practicing, and actively looking for a job.
  • Visit some popular developer conferences where you would be one of the only designers attending. Talk to people and propose your help for their startups. Who knows, you may become the co-creator of some future cool startup!
  • Choose an area where digitalization hasn’t propagated yet and create a design concept using very modern technologies. For instance, people have been growing plants for thousands of years, but data analysis and visualization dramatically changed the efficiency of that process only lately. The agricultural industry has undergone a remarkable transformation thanks to UX design — a crucial element in ensuring that agricultural applications are not just functional but also intuitive and user-friendly. From precision farming to crop monitoring systems, digital tools have revolutionized the way farmers manage their operations.
    Note: You can check the following article for details: “The Evolution of UX Design in Agricultural Applications.”

Don’t wait until someone hands you your chance on a “silver platter.” There are many projects that need the designer’s hands and help but can’t get such help yet. Assist them and then show the results of your work in your first portfolio. It gives you a huge advantage over other candidates who haven’t worked on their portfolios yet!

Preparing For Your First Interviews: Getting A First Job

From what I’ve heard, getting the first job is the biggest problem for a junior designer, so I will focus on this part in more detail.

Applying For A Job

To reach the goal, you should formulate it correctly. It’s already formulated in this case, but most candidates understand it wrong. The right goal here is to be invited to an interview — not to get an offer right now or tell everything about your life in the CV document. You just need to break through the first level of filtering.

Note: Some of these tips are for absolute beginners. Do they sound too obvious to you? Apologies if so. However, all of them are based on my personal experience, so I think there are no tips that I should omit.

  • Send your CV and motivational letter (if required in the job description) from the correct email address. It’s always strange to receive a job application from an email such as ‘sad.batman2006@gmail.com’. Seniors are always responsible for the tasks that junior designers complete, and we want to know that you are a seriously-minded and responsible person to help us do our work. Small details, such as the email address you would use to get in touch, do matter.
  • Use your real name. I’ve had cases where people have used different names in their emails and CVs. I think it’s too obvious why this will look very strange, so I won’t spend time describing it in detail.
  • Skill representations. Use the well-accepted standards. I have seen some CVs created with the help of services such as CV Maker where skills (level of English, how well you know Figma, Illustrator, and other design tools, and so on) were represented as loaders or diagrams. But there are existing standards, so use them in order to be understood better. For instance, if you describe your level of English knowledge, use the CEFR levels (A1/A2, B1/B2, C1/C2). Don’t make people interpret a diagram instead.
  • Check/proofread the text in your email, CV, and portfolio. We expect that you may not know everything about design, but spelling errors don’t demonstrate exactly your desire to learn and your attention to detail. You can use Grammarly or ChatGPT to check your text, but you should not try to substitute your thoughts with some AI-“generated” ideas. Also, make sure to structure well the content of your CV and to format it properly.
  • Read the job description carefully, find matches with your skills, and reflect these in the CV. Recruiters cannot review all the CVs thoroughly. Remember, the goal is to break through the first level of filtering — the recruiter is not a designer and can’t evaluate you and your skills. However, the recruiter can decide whether your CV is relevant to the job description, so it’s very important to tweak the CV by making sure you mention all the skills that you possess and that match the ones found in the specific job description.
  • Don’t count solely on the job application form posted on the company’s website. There were cases when I had no reply after filling out and submitting the official application form but then got an offer after trying to reach a recruiter from that company directly on LinkedIn or via some other available communication channel. So don’t be shy to get in touch directly.
  • Avoid using PDF documents for portfolios or anything else that people need to download before opening. The more time it takes to open and review your portfolio, the less time people will spend checking what’s in it. A link to your portfolio on the web will always work better, and it’s also a much more professional approach! You can use platforms such as Behance (or similar), or you can create your case studies in Figma and paste the shareable link into your CV.

Note: There are many ways to show your portfolio, and Figma is only one of them. For ideas, you can check “Figma Portfolio Templates & Examples” (a curated selection of portfolio templates for Figma). Or even better, you can self-host your portfolio on your own domain and website; however, this may require some more advanced technical skills and knowledge, so you can leave this idea for later.

Completing A Test Task

The test task aims to assess what we can expect from you in the workplace. And this is not just about the quality of your design skills — it’s also about how you will communicate with others and how you will be able to propose practical solutions to problems.

What do I mean by “practical solutions”? In the real world, designers always work within certain limitations (constraints), such as time, budget, team capacity, and so on. So, if you have some bright ideas that are likely very hard to implement, keep these for the interview. The test task is a way to show that you are someone who can define the correct problems and do the proper work, e.g. find the solutions to them.

A few words of advice on how to do exactly that:

  • If you have a chance to speak to the target audience, do it, especially if the test task is to make an existing product better. You don’t have to do complete research, but if it’s a popular product that everyone uses, you can ask your friends about their experience of using it. If it’s not, check what people say on Reddit, in reviews on the Apple App Store, or on Google Play. Find video reviews of this product on YouTube and analyze the comments under the video. Also, take a look at similar products and what people say about them. Defining real problems is a key skill for designers.
    Note: How can we we conduct UX research when there is no or only limited access to users? Vitaly Friedman outlines a few excellent strategies in his article on this topic: “Why Designers Aren’t Understood.”
  • Prioritize features that you see and can reflect on in the test task. You can use the Kano model or another framework, but don’t skip this step! It is sometimes puzzling to see candidates spending a lot of time on dark mode UI mockups but failing to work on the required key features instead.
    Note: The Kano Analysis model is a tool that enables you to understand how customer emotional responses to products or features can be measured and explored.
  • If you need more time, say so. It also will show what your behavior will be when working on a real project. Speaking about the problem at the last moment can bring big troubles to the team. Also — happened in my practice in a few cases — it’s strange to hear:
    • “I didn’t fully complete the test task because I was busy.”
    • OK, if you are too busy (with other things?), then we will have to interview some other candidates.
      My advice is to show dedication and focus toward your current job application assignment.
  • In some cases, the candidates try to go the “extra mile” by doing more things than were initially asked of them, but with lower quality. Unfortunately, It doesn’t work this way. Instead, you need to do less but better. Of course, there could be exclusions in some cases, like when you do sketching and prototyping, where showing rough ideas is perfectly OK. So, try to find the balance between the volume and quality of your work. Showing many (but weak) mockups in order to impress with the volume of your work (instead of the quality) is not a good idea.
  • Sometimes, we ask to redesign a screen as a test task. This is not about using better/shinier UI components. Instead, try to understand the user goals on that screen and then think about the most suitable UI components that you can use to serve these user goals.

Recommendations For The Interview

The interview is the most challenging part because the most optimal way to prepare for it depends on the specific company where you’re applying for the job and the interviewer’s experience. But there are still a few “universal” things you can do in order to increase your chances:

  • If I was restricted to giving only one piece of advice, I would say: Be sincere! It’s not an exam, so don’t try to guess the answer if you don’t know it. No one knows everything, and it’s OK — be honest and it will pay off.
  • Research the company and the role before the interview. Check the company’s portfolio, cases, products, and so on, and even look up the names and titles of designers working there.
    Note: It will help a lot if the company has an AboutThe Team page on its website; but if not, using LinkedIn will probably help, too.) When you have researched the role in detail, it will help you define which of your skills will be a good match and you could then highlight them during the interview.
  • The core questions in a UX design interview are not a secret. Usually, it’s about the design phases, your experience, hobbies, motivation, and so on. Work on these questions and clarify the answers before going to the interview. Just write them down and read them out loudly. Try to check how it sounds. Converting your design experience into exact words requires brain energy, especially if somebody in front of you is waiting for the answer, so do it beforehand, and you’ll feel much better prepared — and calmer.
  • Listen carefully to the questions you are being asked. Ask the interviewer to clarify if you do not understand a question completely. It’s always weird when the candidate gives an answer that is not related to the question you asked.
  • Don’t be late. Do your best to be on time.
    • If it’s an online interview, check the time zones, the communication tools, and everything else. There’s nothing worse than starting Zoom (or another app that you know you’ll need) at the last minute and discovering that it needs an urgent update. Precious minutes will be lost during the update process while the other party will be patiently waiting for you to come online. And you better also check your headphones, microphone, camera, and Bluetooth connection before the start of the meeting.
    • Similarly, if it’s an in-person interview, plan your trip in advance and add some extra time for something unexpected; better if you arrive early than late. The problem is not only about wasting someone’s time; it’s about your emotional balance. If you are late, you will be nervous and make mistakes that you otherwise wouldn’t.
  • Don’t look for a job in the companies of your dreams right from the start. First, pass a few interviews with other companies, get feedback, do some retrospectives, gain some real experience, and be prepared to show your best when you get your chance.
  • Be yourself, but also clearly communicate who you are going to be as people with goals and a plan always make a better impression. Most companies don’t hire juniors — they hire future middle-level and senior designers. And if you feel a certain company where you’re applying for a job would not support you in this way, better try another one. The first few years are the foundation of your future career, so do your best to get into a company where you can grow as a designer.
Conclusion

Thank you for following me so far! Hopefully, you have learned your design tools, worked on your portfolio, and prepared meticulously for your first interviews. If all goes according to plan, sooner or later, you’ll get your first junior UX job. And then you’ll face more challenges, about which I will speak in detail in the second part of my two-part article series.

But before that, do check Further Reading, where I have gathered a few resources that will be very useful if you are just about to begin your UX design career.

Further Reading

Basic Design Resources

A List of Design Resources from the Nielsen Norman Group

Presenting UX Research And Design To Stakeholders: The Power Of Persuasion

Featured Imgs 23

For UX researchers and designers, our journey doesn’t end with meticulously gathered data or well-crafted design concepts saved on our laptops or in the cloud. Our true impact lies in effectively communicating research findings and design concepts to key stakeholders and securing their buy-in for implementing our user-centered solutions. This is where persuasion and communication theory become powerful tools, empowering UX practitioners to bridge the gap between research and action.

I shared a framework for conducting UX research in my previous article on infusing communication theory and UX. In this article, I’ll focus on communication and persuasion considerations for presenting our research and design concepts to key stakeholder groups.

A Word On Persuasion: Guiding Understanding, Not Manipulation

UX professionals can strategically use persuasion techniques to turn complex research results into clear, practical recommendations that stakeholders can understand and act on. It’s crucial to remember that persuasion is about helping people understand what to do, not tricking them. When stakeholders see the value of designing with the user in mind, they become strong partners in creating products and services that truly meet user needs. We’re not trying to manipulate anyone; we’re trying to make sure our ideas get the attention they deserve in a busy world.

The Hovland-Yale Model Of Persuasion

The Hovland-Yale model, a framework for understanding how persuasion works, was developed by Carl Hovland and his team at Yale University in the 1950s. Their research was inspired by World War II propaganda, as they wanted to figure out what made some messages more convincing than others.

In the Hovland-Yale model, persuasion is understood as a process involving the Independent variables of Source, Message, and Audience. The elements of each factor then lead to the Audience having internal mediating processes around the topic, which, if independent variables are strong enough, can strengthen or change attitudes or behaviors. The interplay of the internal mediating processes leads to persuasion or not, which then leads to the observable effect of the communication (or not, if the message is ineffective). The model proposes that if these elements are carefully crafted and applied, the intended change in attitude or behavior (Effect) is more likely to be successful.

The diagram below helps identify the parts of persuasive communication. It shows what you can control as a presenter, how people think about the message and the impact it has. If done well, it can lead to change. I’ll focus exclusively on the independent variables in the far left side of the diagram in this article because, theoretically, this is what you, as the outside source creating a persuasive message, are in control of and, if done well, would lead to the appropriate mediating processes and desired observable effects.

Effective communication can reinforce currently held positions. You don’t always need to change minds when presenting research; much of what we find and present might align with currently held beliefs and support actions our stakeholders are already considering.

Over the years, researchers have explored the usefulness and limitations of this model in various contexts. I’ve provided a list of citations at the end of this article if you are interested in exploring academic literature on the Hovland-Yale model. Reflecting on some of the research findings can help shape how we create and deliver our persuasive communication. Some consistent from academia highlight that:

  • Source credibility significantly influences the acceptance of a persuasive message. A high-credibility source is more persuasive than a low-credibility one.
  • Messages that are logically structured, clear, and relatively concise are more likely to be persuasive.
  • An audience’s attitude change is also dependent on the channel of communication. Mass media is found to be less effective in changing attitudes than face-to-face communication.
  • The audience’s initial attitude, intelligence, and self-esteem have a significant role in the persuasion process. Research suggests that individuals with high intelligence are typically more resistant to persuasion efforts, and those with moderate self-esteem are easier to persuade than those with low or high self-esteem.
  • The effect of persuasive messages tends to fade over time, especially if delivered by a non-credible source. This suggests a need to reinforce even effective messages on a regular basis to maintain an effect.

I’ll cover the impact of each of these bullets on UX research and design presentations in the relevant sections below.

It’s important to note that while the Hovland-Yale model provides valuable insight into persuasive communication, it remains a simplification of a complex process. Actual attitude change and decision-making can be influenced by a multitude of other factors not covered in this model, like emotional states, group dynamics, and more, necessitating a multi-faceted approach to persuasion. However, the model provides a manageable framework to strengthen the communication of UX research findings, with a focus on elements that are within the control of the researcher and product team. I’ll break down the process of presenting findings to various audiences in the following section.

Let’s move into applying the models to our work as UX practitioners with a focus on how the model applies to how we prepare and present our findings to various stakeholders. You can reference the diagram above as needed as we move through the Independent variables.

Applying The Hovland-Yale Model To Presenting Your UX Research Findings

Let’s break down the key parts of the Hovland-Yale model and see how we can use them when presenting our UX research and design ideas.

Source

Revised: The Hovland-Yale model stresses that where a message comes from greatly affects how believable and effective it is. Research shows that a convincing source needs to be seen as dependable, informed, and trustworthy. In UX research, this source is usually the researcher(s) and other UX team members who present findings, suggest actions, lead workshops, and share design ideas. It’s crucial for the UX team to build trust with their audience, which often includes users, stakeholders, and designers.

You can demonstrate and strengthen your credibility throughout the research process and once again when presenting your findings.

How Can You Make Yourself More Credible?

You should start building your expertise and credibility before you even finish your research. Often, stakeholders will have already formed an opinion about your work before you even walk into the room. Here are a couple of ways to boost your reputation before or at the beginning of a project:

Case Studies

A well-written case study about your past work can be a great way to show stakeholders the benefits of user-centered design. Make sure your case studies match what your stakeholders care about. Don’t just tell an interesting story; tell a story that matters to them. Understand their priorities and tailor your case study to show how your UX work has helped achieve goals like higher ROI, happier customers, or lower turnover. Share these case studies as a document before the project starts so stakeholders can review them and get a positive impression of your work.

Thought Leadership

Sharing insights and expertise that your UX team has developed is another way to build credibility. This kind of “thought leadership” can establish your team as the experts in your field. It can take many forms, like blog posts, articles in industry publications, white papers, presentations, podcasts, or videos. You can share this content on your website, social media, or directly with stakeholders.

For example, if you’re about to start a project on gathering customer feedback, share any relevant articles or guides your team has created with your stakeholders before the project kickoff. If you are about to start developing a voice of the customer program and you happen to have Victor or Dana on your team, share their article on creating a VoC to your group of stakeholders prior to the kickoff meeting. [Shameless self-promotion and a big smile emoji].

You can also build credibility and trust while discussing your research and design, both during the project and when you present your final results.

Business Goals Alignment

To really connect with stakeholders, make sure your UX goals and the company’s business goals work together. Always tie your research findings and design ideas back to the bigger picture. This means showing how your work can affect things like customer happiness, more sales, lower costs, or other important business measures. You can even work with stakeholders to figure out which measures matter most to them. When you present your designs, point out how they’ll help the company reach its goals through good UX.

Industry Benchmarks

These days, it’s easier to find data on how other companies in your industry are doing. Use this to your advantage! Compare your findings to these benchmarks or even to your competitors. This can help stakeholders feel more confident in your work. Show them how your research fits in with industry trends or how it uncovers new ways to stand out. When you talk about your designs, highlight how you’ve used industry best practices or made changes based on what you’ve learned from users.

Methodological Transparency

Be open and honest about how you did your research. This shows you know what you’re doing and that you can be trusted. For example, if you were looking into why fewer people are renewing their subscriptions to a fitness app, explain how you planned your research, who you talked to, how you analyzed the data, and any challenges you faced. This transparency helps people accept your research results and builds trust.

Increasing Credibility Through Design Concepts

Here are some specific ways to make your design concepts more believable and trustworthy to stakeholders:

Ground Yourself in Research. You’ve done the research, so use it! Make sure your design decisions are based on your findings and user data. When you present, highlight the data that supports your choices.

Go Beyond Mockups. It’s helpful for stakeholders to see your designs in action. Static mockups are a good start, but try creating interactive prototypes that show how users will move through and use your design. This is especially important if you’re creating something new that stakeholders might have trouble visualizing.

User Quotes and Testimonials. Include quotes or stories from users in your presentation. This makes the process more personal and shows that you’re focused on user needs. You can use these quotes to explain specific design choices.

Before & After Impact. Use visuals or user journey maps to show how your design solution improves the user experience. If you’ve mapped out the current user journey or documented existing problems, show how your new design fixes those problems. Don’t leave stakeholders guessing about your design choices. Briefly explain why you made key decisions and how they help users or achieve business goals. You should have research and stakeholder input to back up your decisions.

Show Your Process. When presenting a more developed concept, show the work that led up to it. Don’t just share the final product. Include early sketches, wireframes, or simple prototypes to show how the design evolved and the reasoning behind your choices. This is especially helpful for executives or stakeholders who haven’t been involved in the whole process.

Be Open to Feedback and Iteration. Work together with stakeholders. Show that you’re open to their feedback and explain how their input can help you improve your designs.

Much of what I’ve covered above are also general best practices for presenting. Remember, these are just suggestions. You don’t have to use every single one to make your presentations more persuasive. Try different things, see what works best for you and your stakeholders, and have fun with it! The goal is to build trust and credibility with your UX team.

Message

The Hovland-Yale model, along with most other communication models, suggests that what you communicate is just as important as how you communicate it. In UX research, your message is usually your insights, data analysis, findings, and recommendations.

I’ve touched on this in the previous section because it’s hard to separate the source (who’s talking) from the message (what they’re saying). For example, building trust involves being transparent about your research methods, which is part of your message. So, some of what I’m about to say might sound familiar.

For this article, let’s define the message as your research findings and everything that goes with them (e.g., what you say in your presentation, the slides you use, other media), as well as your design concepts (how you show your design solutions, including drawings, wireframes, prototypes, and so on).

The Hovland-Yale model says it’s important to make your message easy to understand, relevant, and impactful. For example, instead of just saying,

“30% of users found the signup process difficult.”

you could say,

“30% of users struggled to sign up because the process was too complicated. This could lead to fewer renewals. Making the signup process easier could increase renewals and improve the overall experience.”

Storytelling is also a powerful way to get your message across. Weaving your findings into a narrative helps people connect with your data on a human level and remember your key points. Using real quotes or stories from users makes your presentation even more compelling.

Here are some other tips for delivering a persuasive message:

  • Practice Makes Perfect
    Rehearse your presentation. This will help you smooth out any rough spots, anticipate questions, and feel more confident.
  • Anticipate Concerns
    Think about any objections stakeholders might have and be ready to address them with data.
  • Welcome Feedback
    Encourage open discussion during your presentation. Listen to what stakeholders have to say and show that you’re willing to adapt your recommendations based on their concerns. This builds trust and makes everyone feel like they’re part of the process.
  • Follow Through is Key
    After your presentation, send a clear summary of the main points and action items. This shows you’re professional and makes it easy for stakeholders to refer back to your findings.

When presenting design concepts, it’s important to tell, not just show, what you’re proposing. Stakeholders might not have a deep understanding of UX, so just showing them screenshots might not be enough. Use user stories to walk them through the redesigned experience. This helps them understand how users will interact with your design and what benefits it will bring. Static screens show the “what,” but user stories reveal the “why” and “how.” By focusing on the user journey, you can demonstrate how your design solves problems and improves the overall experience.

For example, if you’re suggesting changes to the search bar and adding tooltips, you could say:

“Imagine a user lands on the homepage and sees the new, larger search bar. They enter their search term and get results. If they see an unfamiliar tool or a new action, they can hover over it to see a brief description.”

Here are some other ways to make your design concepts clearer and more persuasive:

  • Clear Design Language
    Use a consistent and visually appealing design language in your mockups and prototypes. This shows professionalism and attention to detail.
  • Accessibility Best Practices
    Make sure your design is accessible to everyone. This shows that you care about inclusivity and user-centered design.

One final note on the message is that research has found the likelihood of an audience’s attitude change is also dependent on the channel of communication. Mass media is found to be less effective in changing attitudes than face-to-face communication. Distributed teams and remote employees can employ several strategies to compensate for any potential impact reduction of asynchronous communication:

  • Interactive Elements
    Incorporate interactive elements into presentations, such as polls, quizzes, or clickable prototypes. This can increase engagement and make the experience more dynamic for remote viewers.
  • Video Summaries
    Create short video summaries of key findings and recommendations. This adds a personal touch and can help convey nuances that might be lost in text or static slides.
  • Virtual Q&A Sessions
    Schedule dedicated virtual Q&A sessions where stakeholders can ask questions and engage in discussions. This allows for real-time interaction and clarification, mimicking the benefits of face-to-face communication.
  • Follow-up Communication
    Actively follow up with stakeholders after they’ve reviewed the materials. Offer to discuss the content, answer questions, and gather feedback. This demonstrates a commitment to communication and can help solidify key takeaways.

Framing Your Message for Maximum Impact

The way you frame an issue can greatly influence how stakeholders see it. Framing is a persuasion technique that can help your message resonate more deeply with specific stakeholders. Essentially, you want to frame your message in a way that aligns with your stakeholders’ attitudes and values and presents your solution as the next logical step. There are many resources on how to frame messages, as this technique has been used often in public safety and public health research to encourage behavior change. This article discusses applying framing techniques for digital design.

You can also frame issues in a way that motivates your stakeholders. For example, instead of calling usability issues “problems,” I like to call them “opportunities.” This emphasizes the potential for improvement. Let’s say your research on a hospital website finds that the appointment booking process is confusing. You could frame this as an opportunity to improve patient satisfaction and maybe even reduce call center volume by creating a simpler online booking system. This way, your solution is a win-win for both patients and the hospital. Highlighting the positive outcomes of your proposed changes and using language that focuses on business benefits and user satisfaction can make a big difference.

Audience

Understanding your audience’s goals is essential before embarking on any research or design project. It serves as the foundation for tailoring content, supporting decision-making processes, ensuring clarity and focus, enhancing communication effectiveness, and establishing metrics for evaluation.

One specific aspect to consider is securing buy-in from the product and delivery teams prior to beginning any research or design. Without their investment in the outcomes and input on the process, it can be challenging to find stakeholders who see value in a project you created in a vacuum. Engaging with these teams early on helps align expectations, foster collaboration, and ensure that the research and design efforts are informed by the organization’s objectives.

Once you’ve identified your key stakeholders and secured buy-in, you should then Map the Decision-Making Process or understand the decision-making process your audience goes through, including the pain points, considerations, and influencing factors.

  • How are decisions made, and who makes them?
  • Is it group consensus?
  • Are there key voices that overrule all others?
  • Is there even a decision to be made in regard to the work you will do?

Understanding the decision-making process will enable you to provide the necessary information and support at each stage.

Finally, prior to engaging in any work, set clear objectives with your key stakeholders. Your UX team needs to collaborate with the product and delivery teams to establish clear objectives for the research or design project. These objectives should align with the organization’s goals and the audience’s needs.

By understanding your audience’s goals and involving the product and delivery teams from the outset, you can create research and design outcomes that are relevant, impactful, and aligned with the organization’s objectives.

As the source of your message, it’s your job to understand who you’re talking to and how they see the issue. Different stakeholders have different interests, goals, and levels of knowledge. It’s important to tailor your communication to each of these perspectives. Adjust your language, what you emphasize, and the complexity of your message to suit your audience. Technical jargon might be fine for technical stakeholders, but it could alienate those without a technical background.

Audience Characteristics: Know Your Stakeholders

Remember, your audience’s existing opinions, intelligence, and self-esteem play a big role in how persuasive you can be. Research suggests that people with higher intelligence tend to be more resistant to persuasion, while those with moderate self-esteem are easier to persuade than those with very low or very high self-esteem. Understanding your audience is key to giving a persuasive presentation of your UX research and design concepts. Tailoring your communication to address the specific concerns and interests of your stakeholders can significantly increase the impact of your findings.

To truly know your audience, you need information about who you’ll be presenting to, and the more you know, the better. At the very least, you should identify the different groups of stakeholders in your audience. This could include designers, developers, product managers, and executives. If possible, try to learn more about your key stakeholders. You could interview them at the beginning of your process, or you could give them a short survey to gauge their attitudes and behaviors toward the area your UX team is exploring.

Then, your UX team needs to decide the following:

  • How can you best keep all stakeholders engaged and informed as the project unfolds?
  • How will your presentation or concepts appeal to different interests and roles?
  • How can you best encourage discussion and decision-making with the different stakeholders present?
  • Should you hold separate presentations because of the wide range of stakeholders you need to share your findings with?
  • How will you prioritize information?

Your answers to the previous questions will help you focus on what matters most to each stakeholder group. For example, designers might be more interested in usability issues, while executives might care more about the business impact. If you’re presenting to a mixed audience, include a mix of information and be ready to highlight what’s relevant to each group in a way that grabs their attention. Adapt your communication style to match each group’s preferences. Provide technical details for developers and emphasize user experience benefits for executives.

Example

Let’s say you did UX research for a mobile banking app, and your audience includes designers, developers, and product managers.

Designers:

  • Focus on: Design-related findings like what users prefer in the interface, navigation problems, and suggestions for the visual design.
  • How to communicate: Use visuals like heatmaps and user journey maps to show design challenges. Talk about how fixing these issues can make the overall user experience better.

Developers:

  • Focus on: Technical stuff, like performance problems, bugs, or challenges with building the app.
  • How to communicate: Share code snippets or technical details about the problems you found. Discuss possible solutions that the developers can actually build. Be realistic about how much work it will take and be ready to talk about a “minimum viable product” (MVP).

Product Managers:

  • Focus on: Findings that affect how users engage with the app, how long they keep using it, and the overall business goals.
  • How to communicate: Use numbers and data to show how UX improvements can help the business. Explain how the research and your ideas fit into the product roadmap and long-term strategy.

By tailoring your presentation to each group, you make sure your message really hits home. This makes it more likely that they’ll support your UX research findings and work together to make decisions.

The Effect (Impact)

The end goal of presenting your findings and design concepts is to get key stakeholders to take action based on what you learned from users. Make sure the impact of your research is crystal clear. Talk about how your findings relate to business goals, customer happiness, and market success (if those are relevant to your product). Suggest clear, actionable next steps in the form of design concepts and encourage feedback and collaboration from stakeholders. This builds excitement and gets people invested. Make sure to answer any questions and ask for more feedback to show that you value their input. Remember, stakeholders play a big role in the product’s future, so getting them involved increases the value of your research.

The Call to Action (CTA)

Your audience needs to know what you want them to do. End your presentation with a strong call to action (CTA). But to do this well, you need to be clear on what you want them to do and understand any limitations they might have.

For example, if you’re presenting to the CEO, tailor your CTA to their priorities. Focus on the return on investment (ROI) of user-centered design. Show how your recommendations can increase sales, improve customer satisfaction, or give the company a competitive edge. Use clear visuals and explain how user needs translate into business benefits. End with a strong, action-oriented statement, like

“Let’s set up a meeting to discuss how we can implement these user-centered design recommendations to reach your strategic goals.”

If you’re presenting to product managers and business unit leaders, focus on the business goals they care about, like increasing revenue or reducing customer churn. Explain your research findings in terms of ROI. For example, a strong CTA could be:

“Let’s try out the redesigned checkout process and aim for a 10% increase in conversion rates next quarter.”

Remember, the effects of persuasive messages can fade over time, especially if the source isn’t seen as credible. This means you need to keep reinforcing your message to maintain its impact.

Understanding Limitations and Addressing Concerns

Persuasion is about guiding understanding, not tricking people. Be upfront about any limitations your audience might have, like budget constraints or limited development resources. Anticipate their concerns and address them in your CTA. For example, you could say,

“I know implementing the entire redesign might need more resources, so let’s prioritize the high-impact changes we found in our research to improve the checkout process within our current budget.”

By considering both your desired outcome and your audience’s perspective, you can create a clear, compelling, and actionable CTA that resonates with stakeholders and drives user-centered design decisions.

Finally, remember that presenting your research findings and design concepts isn’t the end of the road. The effects of persuasive messages can fade over time. Your team should keep looking for ways to reinforce key messages and decisions as you move forward with implementing solutions. Keep your presentations and concepts in a shared folder, remind people of the reasoning behind decisions, and be flexible if there are multiple ways to achieve the desired outcome. Showing how you’ve addressed stakeholder goals and concerns in your solution will go a long way in maintaining credibility and trust for future projects.

A Tool to Track Your Alignment to the Hovland-Yale Model

You and your UX team are likely already incorporating elements of persuasion into your work. It might be helpful to track how you are doing this to reflect on what works, what doesn’t, and where there are gaps. I’ve provided a spreadsheet in Figure 3 below for you to modify and use as you might see fit. I’ve included sample data to provide an example of what type of information you might want to record. You can set up the structure of a spreadsheet like this as you think about kicking off your next project, or you can fill it in with information from a recently completed project and reflect on what you can incorporate more in the future.

Please use the spreadsheet below as a suggestion and make additions, deletions, or changes as best suited to meet your needs. You don’t need to be dogmatic in adhering to what I’ve covered here. Experiment, find what works best for you, and have fun.

Project Phase Persuasion Element Topic Description Example Notes/
Reflection
Pre-Presentation Audience Stakeholder Group Identify the specific audience segment (e.g., executives, product managers, marketing team) Executives
Message Message Objectives What specific goals do you aim to achieve with each group? (e.g., garner funding, secure buy-in for specific features) Secure funding for continued app redesign
Source Source Credibility How will you establish your expertise and trustworthiness to each group? (e.g., past projects, relevant data) Highlighted successful previous UX research projects & strong user data analysis skills
Message Message Clarity & Relevance Tailor your presentation language and content to resonate with each audience’s interests and knowledge level Presented a concise summary of key findings with a focus on potential ROI and revenue growth for executives
Presentation & Feedback Source Attention Techniques How did you grab each group’s interest? (e.g., visuals, personal anecdotes, surprising data) Opened presentation with a dramatic statistic about mobile banking app usage
Message Comprehension Strategies Did you ensure understanding of key information? (e.g., analogies, visuals, Q&A) Used relatable real-world examples and interactive charts to explain user research findings
Message Emotional Appeals Did you evoke relevant emotions to motivate action? (e.g., fear of missing out, excitement for potential) Highlighted potential revenue growth and improved customer satisfaction with app redesign
Message Retention & Application What steps did you take to solidify key takeaways and encourage action? (e.g., clear call to action, follow-up materials) Ended with a concise call to action for funding approval and provided detailed research reports for further reference
Audience Stakeholder Feedback Record their reactions, questions, and feedback during and after the presentation Executives impressed with user insights, product managers requested specific data breakdowns
Analysis & Reflection Effect Effective Strategies & Outcomes Identify techniques that worked well and their impact on each group Executives responded well to the emphasis on business impact, leading to conditional funding approval
Feedback Improvements for Future Presentations Note areas for improvement in tailoring messages and engaging each stakeholder group Consider incorporating more interactive elements for product managers and diversifying data visualizations for wider appeal
Analysis Quantitative Metrics Track changes in stakeholder attitudes Conducted a follow-up survey to measure stakeholder agreement with design recommendations before and after the presentation Assess effectiveness of the presentation

Figure 3: Example of spreadsheet categories to track the application of the Hovland-Yale model to your presentation of UX Research findings.

References

Foundational Works

  • Hovland, C. I., Janis, I. L., & Kelley, H. H. (1953). Communication and persuasion. New Haven, CT: Yale University Press. (The cornerstone text on the Hovland-Yale model).
  • Weiner, B. J., & Hovland, C. I. (1956). Participating vs. nonparticipating persuasive presentations: A further study of the effects of audience participation. Journal of Abnormal and Social Psychology, 52(2), 105-110. (Examines the impact of audience participation in persuasive communication).
  • Kelley, H. H., & Hovland, C. I. (1958). The communication of persuasive content. Psychological Review, 65(4), 314-320. (Delves into the communication of persuasive messages and their effects).

Contemporary Applications

  • Pfau, M., & Dalton, M. J. (2008). The persuasive effects of fear appeals and positive emotion appeals on risky sexual behavior intentions. Journal of Communication, 58(2), 244-265. (Applies the Hovland-Yale model to study the effectiveness of fear appeals).
  • Chen, G., & Sun, J. (2010). The effects of source credibility and message framing on consumer online health information seeking. Journal of Interactive Advertising, 10(2), 75-88. (Analyzes the impact of source credibility and message framing, concepts within the model, on health information seeking).
  • Hornik, R., & McHale, J. L. (2009). The persuasive effects of emotional appeals: A meta-analysis of research on advertising emotions and consumer behavior. Journal of Consumer Psychology, 19(3), 394-403. (Analyzes the role of emotions in persuasion, a key aspect of the model, in advertising).

Switching It Up With HTML’s Latest Control

Category Image 052

The web is no stranger to taking HTML elements and transforming them to look, act, and feel like something completely different. A common example of this is the switch, or toggle, component. We would hide a checkbox beneath several layers of styles, define the ARIA role as “switch,” and then ship. However, this approach posed certain usability issues around indeterminate states and always felt rather icky. After all, as the saying goes, the best ARIA is no ARIA.

Well, there is new hope for a native HTML switch to catch on.

Safari Technology Preview (TP) 185 and Safari 17.4 released with an under-the-radar feature, a native HTML switch control. It evolves from the hidden-checkbox approach and aims to make the accessibility and usability of the control more consistent.

<!-- This will render a native checkbox --//>
<input type="checkbox" />

<!-- Add the switch attribute to render a switch element --//>
<input type="checkbox" switch />
<input type="checkbox" checked switch />

Communication is one Aspect of the control’s accessibility. Earlier in 2024, there were issues where the switch would not adjust to page zoom levels properly, leading to poor or broken visibility of the control. However, at the time I am writing this, Safari looks to have resolved these issues. Zooming retains the visual cohesion of the switch.

The switch attribute seems to take accessibility needs into consideration. However, this doesn’t prevent us from using it in inaccessible and unusable ways. As mentioned, mixing the required and indeterminate properties between switches and checkboxes can cause unexpected behavior for people trying to navigate the controls. Once again, Adrian sums things up nicely:

“The switch role does not allow mixed states. Ensure your switch never gets set to a mixed state; otherwise, well, problems.”

— Adrian Roselli

Internationalization (I18N): Which Way Is On?

Beyond the accessibility of the switch control, what happens when the switch interacts with different writing modes?

When creating the switch, we had to ensure the use of logical CSS to support different writing modes and directions. This is because a switch being in its right-most position (or inline ending edge) doesn’t mean “on” in some environments. In some languages — e.g., those that are written right-to-left — the left-most position (or inline starting edge) on the switch would likely imply the “on” state.

While we should be writing logical CSS by default now, the new switch control removes that need. This is because the control will adapt to its nearest writing-mode and direction properties. This means that in left-to-right environments, the switch’s right-most position will be its “on” state, and in right-to-left environments, its left-most position will be the “on” state.

See the Pen Safari Switch Control - Styling [forked] by @DanielYuschick.

Final Thoughts

As we continue to push the web forward, it’s natural for our tools to evolve alongside us. The switch control is a welcome addition to HTML for eliminating the checkbox hacks we’ve been resorting to for years.

That said, combining the checkbox and switch into a single input, while being convenient, does raise some concerns about potential markup combinations. Despite this, I believe this can ultimately be resolved with linters or by the browsers themselves under the hood.

Ultimately, having a native approach to switch components can make the accessibility and usability of the control far more consistent — assuming it’s ever supported and adopted for widespread use.

Modern CSS Layouts: You Might Not Need A Framework For That

Category Image 052

Establishing layouts in CSS is something that we, as developers, often delegate to whatever framework we’re most comfortable using. And even though it’s possible to configure a framework to get just what we need out of it, how often have you integrated an entire CSS library simply for its layout features? I’m sure many of us have done it at some point, dating back to the days of 960.gs, Bootstrap, Susy, and Foundation.

Modern CSS features have significantly cut the need to reach for a framework simply for its layout. Yet, I continue to see it happen. Or, I empathize with many of my colleagues who find themselves re-creating the same Grid or Flexbox layout over and over again.

In this article, we will gain greater control over web layouts. Specifically, we will create four CSS classes that you will be able to take and use immediately on just about any project or place where you need a particular layout that can be configured to your needs.

While the concepts we cover are key, the real thing I want you to take away from this is the confidence to use CSS for those things we tend to avoid doing ourselves. Layouts used to be a challenge on the same level of styling form controls. Certain creative layouts may still be difficult to pull off, but the way CSS is designed today solves the burdens of the established layout patterns we’ve been outsourcing and re-creating for many years.

What We’re Making

We’re going to establish four CSS classes, each with a different layout approach. The idea is that if you need, say, a fluid layout based on Flexbox, you have it ready. The same goes for the three other classes we’re making.

And what exactly are these classes? Two of them are Flexbox layouts, and the other two are Grid layouts, each for a specific purpose. We’ll even extend the Grid layouts to leverage CSS Subgrid for when that’s needed.

Within those two groups of Flexbox and Grid layouts are two utility classes: one that auto-fills the available space — we’re calling these “fluid” layouts — and another where we have greater control over the columns and rows — we’re calling these “repeating” layouts.

Finally, we’ll integrate CSS Container Queries so that these layouts respond to their own size for responsive behavior rather than the size of the viewport. Where we’ll start, though, is organizing our work into Cascade Layers, which further allow you to control the level of specificity and prevent style conflicts with your own CSS.

Setup: Cascade Layers & CSS Variables

A technique that I’ve used a few times is to define Cascade Layers at the start of a stylesheet. I like this idea not only because it keeps styles neat and organized but also because we can influence the specificity of the styles in each layer by organizing the layers in a specific order. All of this makes the utility classes we’re making easier to maintain and integrate into your own work without running into specificity battles.

I think the following three layers are enough for this work:

@layer reset, theme, layout;

Notice the order because it really, really matters. The reset layer comes first, making it the least specific layer of the bunch. The layout layer comes in at the end, making it the most specific set of styles, giving them higher priority than the styles in the other two layers. If we add an unlayered style, that one would be added last and thus have the highest specificity.

Related: “Getting Started With Cascade Layers” by Stephanie Eckles.

Let’s briefly cover how we’ll use each layer in our work.

Reset Layer

The reset layer will contain styles for any user agent styles we want to “reset”. You can add your own resets here, or if you already have a reset in your project, you can safely move on without this particular layer. However, do remember that un-layered styles will be read last, so wrap them in this layer if needed.

I’m just going to drop in the popular box-sizing declaration that ensures all elements are sized consistently by the border-box in accordance with the CSS Box Model.

@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
  }

  body {
    margin: 0;
  }
}

Theme Layer

This layer provides variables scoped to the :root element. I like the idea of scoping variables this high up the chain because layout containers — like the utility classes we’re creating — are often wrappers around lots of other elements, and a global scope ensures that the variables are available anywhere we need them. That said, it is possible to scope these locally to another element if you need to.

Now, whatever makes for “good” default values for the variables will absolutely depend on the project. I’m going to set these with particular values, but do not assume for a moment that you have to stick with them — this is very much a configurable system that you can adapt to your needs.

Here are the only three variables we need for all four layouts:

@layer theme {
  :root {
    --layout-fluid-min: 35ch;
    --layout-default-repeat: 3;
    --layout-default-gap: 3vmax;
  }
}

In order, these map to the following:

Notice: The variables are prefixed with layout-, which I’m using as an identifier for layout-specific values. This is my personal preference for structuring this work, but please choose a naming convention that fits your mental model — naming things can be hard!

Layout Layer

This layer will hold our utility class rulesets, which is where all the magic happens. For the grid, we will include a fifth class specifically for using CSS Subgrid within a grid container for those possible use cases.

@layer layout {  
  .repeating-grid {}
  .repeating-flex {}
  .fluid-grid {}
  .fluid-flex {}

  .subgrid-rows {}
}

Now that all our layers are organized, variables are set, and rulesets are defined, we can begin working on the layouts themselves. We will start with the “repeating” layouts, one based on CSS Grid and the other using Flexbox.

Repeating Grid And Flex Layouts

I think it’s a good idea to start with the “simplest” layout and scale up the complexity from there. So, we’ll tackle the “Repeating Grid” layout first as an introduction to the overarching technique we will be using for the other layouts.

Repeating Grid

If we head into the @layout layer, that’s where we’ll find the .repeating-grid ruleset, where we’ll write the styles for this specific layout. Essentially, we are setting this up as a grid container and applying the variables we created to it to establish layout columns and spacing between them.

.repeating-grid {
  display: grid;
  grid-template-columns: repeat(var(--layout-default-repeat), 1fr);
  gap: var(--layout-default-gap);
}

It’s not too complicated so far, right? We now have a grid container with three equally sized columns that take up one fraction (1fr) of the available space with a gap between them.

This is all fine and dandy, but we do want to take this a step further and turn this into a system where you can configure the number of columns and the size of the gap. I’m going to introduce two new variables scoped to this grid:

  • --_grid-repeat: The number of grid columns.
  • --_repeating-grid-gap: The amount of space between grid items.

Did you notice that I’ve prefixed these variables with an underscore? This was actually a JavaScript convention to specify variables that are “private” — or locally-scoped — before we had const and let to help with that. Feel free to rename these however you see fit, but I wanted to note that up-front in case you’re wondering why the underscore is there.

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(var(--layout-default-repeat), 1fr);
  gap: var(--layout-default-gap);
}

Notice: These variables are set to the variables in the @theme layer. I like the idea of assigning a global variable to a locally-scoped variable. This way, we get to leverage the default values we set in @theme but can easily override them without interfering anywhere else the global variables are used.

Now let’s put those variables to use on the style rules from before in the same .repeating-grid ruleset:

.repeating-grid {
  --_grid-repeat: var(--grid-repeat, var(--layout-default-repeat));
  --_repeating-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(var(--_grid-repeat), 1fr);
  gap: var(--_repeating-grid-gap);
}

What happens from here when we apply the .repeating-grid to an element in HTML? Let’s imagine that we are working with the following simplified markup:

<section class="repeating-grid">
  <div></div>
  <div></div>
  <div></div>
</section>

If we were to apply a background-color and height to those divs, we would get a nice set of boxes that are placed into three equally-sized columns, where any divs that do not fit on the first row automatically wrap to the next row.

Time to put the process we established with the Repeating Grid layout to use in this Repeating Flex layout. This time, we jump straight to defining the private variables on the .repeating-flex ruleset in the @layout layer since we already know what we’re doing.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));
}

Again, we have two locally-scoped variables used to override the default values assigned to the globally-scoped variables. Now, we apply them to the style declarations.

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);
}

We’re only using one of the variables to set the gap size between flex items at the moment, but that will change in a bit. For now, the important thing to note is that we are using the flex-wrap property to tell Flexbox that it’s OK to let additional items in the layout wrap into multiple rows rather than trying to pack everything in a single row.

But once we do that, we also have to configure how the flex items shrink or expand based on whatever amount of available space is remaining. Let’s nest those styles inside the parent ruleset:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

If you’re wondering why I’m using the universal selector (*), it’s because we can’t assume that the layout items will always be divs. Perhaps they are <article> elements, <section>s, or something else entirely. The child combinator (>) ensures that we’re only selecting elements that are direct children of the utility class to prevent leakage into other ancestor styles.

The flex shorthand property is one of those that’s been around for many years now but still seems to mystify many of us. Before we unpack it, did you also notice that we have a new locally-scoped --_gap-repeater-calc variable that needs to be defined? Let’s do this:

.repeating-flex {
  --_flex-repeat: var(--flex-repeat, var(--layout-default-repeat));
  --_repeating-flex-gap: var(--flex-gap, var(--layout-default-gap));

  /* New variables */
  --_gap-count: calc(var(--_flex-repeat) - 1);
  --_gap-repeater-calc: calc(
    var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count)
  );

  display: flex;
  flex-wrap: wrap;
  gap: var(--_repeating-flex-gap);

  > * {
    flex: 1 1 calc((100% / var(--_flex-repeat)) - var(--_gap-repeater-calc));
  }
}

Whoa, we actually created a second variable that --_gap-repeater-calc can use to properly calculate the third flex value, which corresponds to the flex-basis property, i.e., the “ideal” size we want the flex items to be.

If we take out the variable abstractions from our code above, then this is what we’re looking at:

.repeating-flex {
  display: flex;
  flex-wrap: wrap;
  gap: 3vmax

  > * {
    flex: 1 1 calc((100% / 3) - calc(3vmax / 3 * 2));
  }
}

Hopefully, this will help you see what sort of math the browser has to do to size the flexible items in the layout. Of course, those values change if the variables’ values change. But, in short, elements that are direct children of the .repeating-flex utility class are allowed to grow (flex-grow: 1) and shrink (flex-shrink: 1) based on the amount of available space while we inform the browser that the initial size (i.e., flex-basis) of each flex item is equal to some calc()-ulated value.

Because we had to introduce a couple of new variables to get here, I’d like to at least explain what they do:

  • --_gap-count: This stores the number of gaps between layout items by subtracting 1 from --_flex-repeat. There’s one less gap in the number of items because there’s no gap before the first item or after the last item.
  • --_gap-repeater-calc: This calculates the total gap size based on the individual item’s gap size and the total number of gaps between items.

From there, we calculate the total gap size more efficiently with the following formula:

calc(var(--_repeating-flex-gap) / var(--_flex-repeat) * var(--_gap-count))

Let’s break that down further because it’s an inception of variables referencing other variables. In this example, we already provided our repeat-counting private variable, which falls back to the default repeater by setting the --layout-default-repeat variable.

This sets a gap, but we’re not done yet because, with flexible containers, we need to define the flex behavior of the container’s direct children so that they grow (flex-grow: 1), shrink (flex-shrink: 1), and with a flex-basis value that is calculated by multiplying the repeater by the total number of gaps between items.

Next, we divide the individual gap size (--_repeating-flex-gap) by the number of repetitions (--_flex-repeat)) to equally distribute the gap size between each item in the layout. Then, we multiply that gap size value by one minus the total number of gaps with the --_gap-count variable.

And that concludes our repeating grids! Pretty fun, or at least interesting, right? I like a bit of math.

Before we move to the final two layout utility classes we’re making, you might be wondering why we want so many abstractions of the same variable, as we start with one globally-scoped variable referenced by a locally-scoped variable which, in turn, can be referenced and overridden again by yet another variable that is locally scoped to another ruleset. We could simply work with the global variable the whole time, but I’ve taken us through the extra steps of abstraction.

I like it this way because of the following:

  1. I can peek at the HTML and instantly see which layout approach is in use: .repeating-grid or .repeating-flex.
  2. It maintains a certain separation of concerns that keeps styles in order without running into specificity conflicts.

See how clear and understandable the markup is:

<section class="repeating-flex footer-usps">
  <div></div>
  <div></div>
  <div></div>
</section>

The corresponding CSS is likely to be a slim ruleset for the semantic .footer-usps class that simply updates variable values:

.footer-usps {
  --flex-repeat: 3;
  --flex-gap: 2rem;
}

This gives me all of the context I need: the type of layout, what it is used for, and where to find the variables. I think that’s handy, but you certainly could get by without the added abstractions if you’re looking to streamline things a bit.

Fluid Grid And Flex Layouts

All the repeating we’ve done until now is fun, and we can manipulate the number of repeats with container queries and media queries. But rather than repeating columns manually, let’s make the browser do the work for us with fluid layouts that automatically fill whatever empty space is available in the layout container. We may sacrifice a small amount of control with these two utilities, but we get to leverage the browser’s ability to “intelligently” place layout items with a few CSS hints.

Fluid Grid

Once again, we’re starting with the variables and working our way to the calculations and style rules. Specifically, we’re defining a variable called --_fluid-grid-min that manages a column’s minimum width.

Let’s take a rather trivial example and say we want a grid column that’s at least 400px wide with a 20px gap. In this situation, we’re essentially working with a two-column grid when the container is greater than 820px wide. If the container is narrower than 820px, the column stretches out to the container’s full width.

If we want to go for a three-column grid instead, the container’s width should be about 1240px wide. It’s all about controlling the minimum sizing values in the gap.

.fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));
}

That establishes the variables we need to calculate and set styles on the .fluid-grid layout. This is the full code we are unpacking:

 .fluid-grid {
  --_fluid-grid-min: var(--fluid-grid-min, var(--layout-fluid-min));
  --_fluid-grid-gap: var(--grid-gap, var(--layout-default-gap));

  display: grid;
  grid-template-columns: repeat(
    auto-fit,
    minmax(min(var(--_fluid-grid-min), 100%), 1fr)
  );
  gap: var(--_fluid-grid-gap);
}

The display is set to grid, and the gap between items is based on the --fluid-grid-gap variable. The magic is taking place in the grid-template-columns declaration.

This grid uses the repeat() function just as the .repeating-grid utility does. By declaring auto-fit in the function, the browser automatically packs in as many columns as it possibly can in the amount of available space in the layout container. Any columns that can’t fit on a line simply wrap to the next line and occupy the full space that is available there.

Then there’s the minmax() function for setting the minimum and maximum width of the columns. What’s special here is that we’re nesting yet another function, min(), within minmax() (which, remember, is nested in the repeat() function). This a bit of extra logic that sets the minimum width value of each column somewhere in a range between --_fluid-grid-min and 100%, where 100% is a fallback for when --_fluid-grid-min is undefined or is less than 100%. In other words, each column is at least the full 100% width of the grid container.

The “max” half of minmax() is set to 1fr to ensure that each column grows proportionally and maintains equally sized columns.

See the Pen Fluid grid [forked] by utilitybend.

That’s it for the Fluid Grid layout! That said, please do take note that this is a strong grid, particularly when it is combined with modern relative units, e.g. ch, as it produces a grid that only scales from one column to multiple columns based on the size of the content.

Fluid Flex

We pretty much get to re-use all of the code we wrote for the Repeating Flex layout for the Fluid Flex layout, but only we’re setting the flex-basis of each column by its minimum size rather than the number of columns.

.fluid-flex {
  --_fluid-flex-min: var(--fluid-flex-min, var(--layout-fluid-min));
  --_fluid-flex-gap: var(--flex-gap, var(--layout-default-gap));

  display: flex;
  flex-wrap: wrap;
  gap: var(--_fluid-flex-gap);

  > * {
    flex: 1 1 var(--_fluid-flex-min);
  }
}

That completes the fourth and final layout utility — but there’s one bonus class we can create to use together with the Repeating Grid and Fluid Grid utilities for even more control over each layout.

Optional: Subgrid Utility

Subgrid is handy because it turns any grid item into a grid container of its own that shares the parent container’s track sizing to keep the two containers aligned without having to redefine tracks by hand. It’s got full browser support and makes our layout system just that much more robust. That’s why we can set it up as a utility to use with the Repeating Grid and Fluid Grid layouts if we need any of the layout items to be grid containers for laying out any child elements they contain.

Here we go:

.subgrid-rows {
  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 4);
    grid-template-rows: subgrid;
  }
}

We have two new variables, of course:

  • --subgrid-gap: The vertical gap between grid items.
  • --subgrid-rows The number of grid rows defaulted to 4.

We have a bit of a challenge: How do we control the subgrid items in the rows? I see two possible methods.

Method 1: Inline Styles

We already have a variable that can technically be used directly in the HTML as an inline style:

<section class="fluid-grid subgrid-rows" style="--subgrid-rows: 4;">
  <!-- items -->
</section>

This works like a charm since the variable informs the subgrid how much it can grow.

Method 2: Using The :has() Pseudo-Class

This approach leads to verbose CSS, but sacrificing brevity allows us to automate the layout so it handles practically anything we throw at it without having to update an inline style in the markup.

Check this out:

.subgrid-rows {
  &:has(> :nth-child(1):last-child) { --subgrid-rows: 1; }
  &:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }
  &:has(> :nth-child(3):last-child) { --subgrid-rows: 3; }
  &:has(> :nth-child(4):last-child) { --subgrid-rows: 4; }
  &:has(> :nth-child(5):last-child) { --subgrid-rows: 5; }
  /* etc. */

  > * {
    display: grid;
    gap: var(--subgrid-gap, 0);
    grid-row: auto / span var(--subgrid-rows, 5);
    grid-template-rows: subgrid;
  }
}

The :has() selector checks if a subgrid row is the last child item in the container when that item is either the first, second, third, fourth, fifth, and so on item. For example, the second declaration:

&:has(> :nth-child(2):last-child) { --subgrid-rows: 2; }

…is pretty much saying, “If this is the second subgrid item and it happens to be the last item in the container, then set the number of rows to 2.”

Whether this is too heavy-handed, I don’t know; but I love that we’re able to do it in CSS.

The final missing piece is to declare a container on our children. Let’s give the columns a general class name, .grid-item, that we can override if we need to while setting each one as a container we can query for the sake of updating its layout when it is a certain size (as opposed to responding to the viewport’s size in a media query).

:is(.fluid-grid:not(.subgrid-rows),
.repeating-grid:not(.subgrid-rows),
.repeating-flex, .fluid-flex) {
    > * {
    container: var(--grid-item-container, grid-item) / inline-size;
  }
}

That’s a wild-looking selector, but the verbosity is certainly kept to a minimum thanks to the :is() pseudo-class, which saves us from having to write this as a larger chain selector. It essentially selects the direct children of the other utilities without leaking into .subgrid-rows and inadvertently selecting its direct children.

The container property is a shorthand that combines container-name and container-type into a single declaration separated by a forward slash (/). The name of the container is set to one of our variables, and the type is always its inline-size (i.e., width in a horizontal writing mode).

The container-type property can only be applied to grid containers — not grid items. This means we’re unable to combine it with the grid-template-rows: subgrid value, which is why we needed to write a more complex selector to exclude those instances.

Demo

Check out the following demo to see how everything comes together.

See the Pen Grid system playground [forked] by utilitybend.

The demo is pulling in styles from another pen that contains the full CSS for everything we made together in this article. So, if you were to replace the .fluid-flex classname from the parent container in the HTML with another one of the layout utilities, the layout will update accordingly, allowing you to compare them.

Those classes are the following:

  • .repeating-grid,
  • .repeating-flex,
  • .fluid-grid,
  • .fluid-flex.

And, of course, you have the option of turning any grid items into grid containers using the optional .subgrid-rows class in combination with the .repeating-grid and .fluid-grid utilities.

Conclusion: Write Once And Repurpose

This was quite a journey, wasn’t it? It might seem like a lot of information, but we made something that we only need to write once and can use practically anywhere we need a certain type of layout using modern CSS approaches. I strongly believe these utilities can not only help you in a bunch of your work but also cut any reliance on CSS frameworks that you may be using simply for its layout configurations.

This is a combination of many techniques I’ve seen, one of them being a presentation Stephanie Eckles gave at CSS Day 2023. I love it when people handcraft modern CSS solutions for things we used to work around. Stephanie’s demonstration was clean from the start, which is refreshing as so many other areas of web development are becoming ever more complex.

After learning a bunch from CSS Day 2023, I played with Subgrid on my own and published different ideas from my experiments. That’s all it took for me to realize how extensible modern CSS layout approaches are and inspired me to create a set of utilities I could rely on, perhaps for a long time.

By no means am I trying to convince you or anyone else that these utilities are perfect and should be used everywhere or even that they’re better than <framework-du-jour>. One thing that I do know for certain is that by experimenting with the ideas we covered in this article, you will get a solid feel of how CSS is capable of making layout work much more convenient and robust than ever.

Create something out of this, and share it in the comments if you’re willing — I’m looking forward to seeing some fresh ideas!

Solving Media Object Float Issues With CSS Block Formatting Contexts

Category Image 052

Let’s imagine we’re making a small component. It can be anything, really, but let’s use a media object as an example. Nicole Sullivan had a solid definition of media objects from way back in 2010, and you probably already know the pattern well: some form of media (often an image) on the left and text beside it on the right. The media could be an image or a video, for example.

This is the basic HTML for the layout, minimized for brevity:

<section class="container">
  <article class="float-left">
    <img src="https://picsum.photos/100">
      <p>I've never had to cook or clean since I discovered Xyz. They perform all my tasks for me. I recommend them.</p>
      <h3>Dan Somore</h3>
  </article>

  <!-- more articles -->

</section>

This HTML gives us a <section> element that is the container for four <article> elements, where each one is a testimonial container that holds an <img> and a <div> with a block of text — our media objects.

Let’s apply some light styling in CSS:

/* Give the parent container breathing room */
.container {
  padding: 20px;
}

/* 
  Styles for each testimonial container 
  Each container is floated left
*/
.float-left {
  border: 2px solid blue;
  background-color: transparent;
  float: left;
  width: 45%;
  min-height: 150px;
  margin-bottom: 20px;
  margin-right: 20px;
}

/* Testimonial images are floated left */
img {
  float: left;
  margin-right: 10px;
}

This code is by no means perfect. In fact, it introduces the wrapping and overflow issues we’re about to discuss. We will look at these issues together before getting into solutions.

Issue 1: Height Collapsing

When an element is floated in its container, it exits its normal document flow and into a floated position, making no contributions to the container’s height. In a container of many floated media objects, the container element’s height is collapsed to contain only non-floated elements. The collapsed height might be inconspicuous in containers without a border or non-floated elements and could disrupt the layout of other elements after a media object container. However, this issue can be easily discovered if there is a non-floated element in the container, among other floated elements.

Let’s add a border to the parent container to see the height-collapsing effect.

The height of the content is what influences the height of the testimonial container. If the image were in the container’s flow, it would be taller than the text, and the container would adjust to it. But, alas, that’s not the case since we introduced a block formatting context when floating the image.

The popular solution with a single line of CSS on the testimonial’s parent container:


.container {
  overflow: auto;
}

The BFC this generates establishes a new document flow within the page’s root element, containing all the container's child elements, including floated media objects. It effectively prevents the testimonial elements from being displaced beyond the parent container’s borders — no extra divs or pseudo-elements are needed like the clearfix approach.

See the Pen Float Solutions: overflow: auto [forked] by Geoff Graham.

That certainly gets the job done! But I want to show you one more way to do this because I believe it’s the best of the bunch.

The Best Solution: display: flow-root

display: flow-root was introduced to address inconsistencies associated with using overflow for generating BFCs. In fact, display: flow-root was explicitly designed to produce BFC, while the overflow property is designed to manage content that surpasses its container. Consequently, overflow can induce unintended side effects, from unwanted scrollbars to data loss.

That’s why I recommend using display: flow-root. It is meant to create a BFC when you need it, whereas the other solutions are more like workarounds.

Conclusion

CSS block formatting contexts are great because they allow you to leave the main document flow, allowing elements to interact differently in a layout. But, of course, those different interactions can feel like buggy behavior if you’re unaware that you’re actually working in a different formatting context.

This is exactly why we have modern layout techniques like Flexbox and Grid. Before we had them, floats were a nice trick for faking columns. But the BFC they created wasn’t so nice. Hence clever workarounds like the clearfix to create a BFC to wrangle the other BFC.

Perhaps the bigger takeaway from all this, though, is to evaluate your layout strategy. If you’re reaching for a float, is it really the best option for what you’re trying to do? Because if so, you may as well embrace the natural text-wrapping behavior rather than trying to fight it. And if you don’t want to fight it, that’s a sure sign you ought to reach for a more modern layout technique, like Flexbox or Grid.

Resources

Further Reading On SmashingMag

How To Draw Radar Charts In Web

Category Image 052

I got to work with a new type of chart for data visualization called a radar chart when a project asked for it. It was new to me, but the idea is that there is a circular, two-dimensional circle with plots going around the chart. Rather than simple X and Y axes, each plot on a radar chart is its own axis, marking a spot between the outer edge of the circle and the very center of it. The plots represent some sort of category, and when connecting them together, they are like vertices that form shapes to help see the relationship of category values, not totally unlike the vectors in an SVG.

Sometimes, the radar chart is called a spider chart, and it’s easy to see why. The axes that flow outward intersect with the connected plots and form a web-like appearance. So, if your Spidey senses were tingling at first glance, you know why.

You already know where we’re going with this: We’re going to build a radar chart together! We’ll work from scratch with nothing but HTML, CSS, and JavaScript. But before we go there, it’s worth noting a couple of things about radar charts.

First, you don’t have to build them from scratch. Chart.js and D3.js are readily available with convenient approaches that greatly simplify the process. Seeing as I needed just one chart for the project, I decided against using a library and took on the challenge of making it myself. I learned something new, and hopefully, you do as well!

Second, there are caveats to using radar charts for data visualization. While they are indeed effective, they can also be difficult to read when multiple series stack up. The relationships between plots are not nearly as decipherable as, say, bar charts. The order of the categories around the circle affects the overall shape, and the scale between series has to be consistent for drawing conclusions.

That all said, let’s dive in and get our hands sticky with data plots.

The Components

The thing I like immediately about radar charts is that they are inherently geometrical. Connecting plots produces a series of angles that form polygon shapes. The sides are straight lines. And CSS is absolutely wonderful for working with polygons given that we have the CSS polygon() function for drawing them by declaring as many points as we need in the function’s arguments.

We will start with a pentagonal-shaped chart with five data categories.

See the Pen Radar chart (Pentagon) [forked] by Preethi Sam.

There are three components we need to establish in HTML before we work on styling. Those would be:

  1. Grids: These provide the axes over which the diagrams are drawn. It’s the spider web of the bunch.
  2. Graphs: These are the polygons we draw with the coordinates of each data plot before coloring them in.
  3. Labels: The text that identifies the categories along the graphs’ axes.

Here’s how I decided to stub that out in HTML:

<!-- GRIDS -->
<div class="wrapper">
  <div class="grids polygons">
    <div></div>
  </div>
  <div class="grids polygons">
    <div></div>
  </div>
  <div class="grids polygons">
    <div></div>
  </div>
</div>

<!-- GRAPHS -->
<div class="wrapper">
  <div class="graphs polygons">
    <div><!-- Set 1 --></div>
  </div>
  <div class="graphs polygons">
    <div><!-- Set 2 --></div>
  </div>
  <div class="graphs polygons">
    <div><!-- Set 3 --></div>
  </div>
  <!-- etc. -->
</div>

<!-- LABELS -->
<div class="wrapper">
  <div class="labels">Data A</div>
  <div class="labels">Data B</div>
  <div class="labels">Data C</div>
  <div class="labels">Data D</div>
  <div class="labels">Data E</div>
  <!-- etc. -->
</div>

I’m sure you can read the markup and see what’s going on, but we’ve got three parent elements (.wrapper) that each holds one of the main components. The first parent contains the .grids, the second parent contains the .graphs, and the third parent contains the .labels.

Base Styles

We’ll start by setting up a few color variables we can use to fill things in as we go:

:root {
  --color1: rgba(78, 36, 221, 0.6); /* graph set 1 */
  --color2: rgba(236, 19, 154, 0.6); /* graph set 2 */
  --color3: rgba(156, 4, 223, 0.6); /* graph set 3 */
  --colorS: rgba(255, 0, 95, 0.1); /* graph shadow */
}

Our next order of business is to establish the layout. CSS Grid is a solid approach for this because we can place all three grid items together on the grid in just a couple of lines:

/* Parent container */
.wrapper { display: grid; }

/* Placing elements on the grid */
.wrapper > div {
  grid-area: 1 / 1; /* There's only one grid area to cover */
}

Let’s go ahead and set a size on the grid items. I’m using a fixed length value of 300px, but you can use any value you need and variablize it if you plan on using it in other places. And rather than declaring an explicit height, let’s put the burden of calculating a height on CSS using aspect-ratio to form perfect squares.

/* Placing elements on the grid */
.wrapper div {
  aspect-ratio: 1 / 1;
  grid-area: 1 / 1;
  width: 300px;
}

We can’t see anything just yet. We’ll need to color things in:

/* ----------
Graphs
---------- */
.graphs:nth-of-type(1) > div { background: var(--color1); }
.graphs:nth-of-type(2) > div { background: var(--color2); }
.graphs:nth-of-type(3) > div { background: var(--color3); }

.graphs {
  filter: 
    drop-shadow(1px 1px 10px var(--colorS))
    drop-shadow(-1px -1px 10px var(--colorS))
    drop-shadow(-1px 1px 10px var(--colorS))
    drop-shadow(1px -1px 10px var(--colorS));
}

/* --------------
Grids 
-------------- */
.grids {
  filter: 
    drop-shadow(1px 1px 1px #ddd)
    drop-shadow(-1px -1px 1px #ddd)
    drop-shadow(-1px 1px 1px #ddd)
    drop-shadow(1px -1px 1px #ddd);
    mix-blend-mode: multiply;
}

.grids > div { background: white; }

Oh, wait! We need to set widths on the grids and polygons for them to take shape:

.grids:nth-of-type(2) { width: 66%; }
.grids:nth-of-type(3) { width: 33%; }

/* --------------
Polygons 
-------------- */
.polygons { place-self: center; }
.polygons > div { width: 100%; }

Since we’re already here, I’m going to position the labels a smidge and give them width:

/* --------------
Labels
-------------- */
.labels:first-of-type { inset-block-sptart: -10%; }

.labels {
  height: 1lh;
  position: relative;
  width: max-content;
}

We still can’t see what’s going on, but we can if we temporarily draw borders around elements.

See the Pen Radar chart layout [forked] by Preethi Sam.

All combined, it doesn’t look all that great so far. Basically, we have a series of overlapping grids followed by perfectly square graphs stacked right on top of one another. The labels are off in the corner as well. We haven’t drawn anything yet, so this doesn’t bother me for now because we have the HTML elements we need, and CSS is technically establishing a layout that should come together as we start plotting points and drawing polygons.

More specifically:

  • The .wrapper elements are displayed as CSS Grid containers.
  • The direct children of the .wrapper elements are divs placed in the exact same grid-area. This is causing them to stack one right on top of the other.
  • The .polygons are centered (place-self: center).
  • The child divs in the .polygons take up the full width (width:100%).
  • Every single div is 300px wide and squared off with a one-to-one aspect-ratio.
  • We’re explicitly declaring a relative position on the .labels. This way, they can be automatically positioned when we start working in JavaScript.

The rest? Simply apply some colors as backgrounds and drop shadows.

Calculating Plot Coordinates

Don’t worry. We are not getting into a deep dive about polygon geometry. Instead, let’s take a quick look at the equations we’re using to calculate the coordinates of each polygon’s vertices. You don’t have to know these equations to use the code we’re going to write, but it never hurts to peek under the hood to see how it comes together.

x1 = x + cosθ1 = cosθ1 if x=0
y1 = y + sinθ1 = sinθ1 if y=0
x2 = x + cosθ2 = cosθ2 if x=0
y2 = y + sinθ2 = sinθ2 if y=0
etc.

x, y = center of the polygon (assigned (0, 0) in our examples)

x1, x2… = x coordinates of each vertex (vertex 1, 2, and so on)
y1, y2… = y coordinates of each vertex
θ1, θ2… = angle each vertex makes to the x-axis

We can assume that 𝜃 is 90deg (i.e., 𝜋/2) since a vertex can always be placed right above or below the center (i.e., Data A in this example). The rest of the angles can be calculated like this:

n = number of sides of the polygon

𝜃1 = 𝜃0 + 2𝜋/𝑛 = 𝜋/2 + 2𝜋/𝑛
𝜃2 = 𝜃0 + 4𝜋/𝑛 = 𝜋/2 + 4𝜋/𝑛
𝜃3 = 𝜃0 + 6𝜋/𝑛 = 𝜋/2 + 6𝜋/𝑛
𝜃3 = 𝜃0 + 8𝜋/𝑛 = 𝜋/2 + 8𝜋/𝑛
𝜃3 = 𝜃0 + 10𝜋/𝑛 = 𝜋/2 + 10𝜋/𝑛

Armed with this context, we can solve for our x and y values:

x1 = cos(𝜋/2 + 2𝜋/# sides)
y1 = sin(𝜋/2 + 2𝜋/# sides)
x2 = cos(𝜋/2 + 4𝜋/# sides)
y2 = sin(𝜋/2 + 4𝜋/# sides)
etc.

The number of sides depends on the number of plots we need. We said up-front that this is a pentagonal shape, so we’re working with five sides in this particular example.

x1 = cos(𝜋/2 + 2𝜋/5)
y1 = sin(𝜋/2 + 2𝜋/5)
x2 = cos(𝜋/2 + 4𝜋/5)
y2 = sin(𝜋/2 + 4𝜋/5)
etc.
Drawing Polygons With JavaScript

Now that the math is accounted for, we have what we need to start working in JavaScript for the sake of plotting the coordinates, connecting them together, and painting in the resulting polygons.

For simplicity’s sake, we will leave the Canvas API out of this and instead use regular HTML elements to draw the chart. You can, however, use the math outlined above and the following logic as the foundation for drawing polygons in whichever language, framework, or API you prefer.

OK, so we have three types of components to work on: grids, graphs, and labels. We start with the grid and work up from there. In each case, I’ll simply drop in the code and explain what’s happening.

Drawing The Grid

// Variables
let sides = 5; // # of data points
let units = 1; // # of graphs + 1
let vertices = (new Array(units)).fill(""); 
let percents = new Array(units);
percents[0] = (new Array(sides)).fill(100); // for the polygon's grid component
let gradient = "conic-gradient(";
let angle = 360/sides;

// Calculate vertices
with(Math) { 
  for(i=0, n = 2 * PI; i < sides; i++, n += 2 * PI) {
    for(j=0; j < units; j++) {
      let x = ( round(cos(-1 * PI/2 + n/sides) * percents[j][i]) + 100 ) / 2; 
      let y = ( round(sin(-1 * PI/2 + n/sides) * percents[j][i]) + 100 ) / 2; 
      vertices[j] += ${x}% ${y} ${i == sides - 1 ? '%':'%, '};
  }
  gradient += white ${
    (angle &#42; (i+1)) - 1}deg,
    #ddd ${ (angle &#42; (i+1)) - 1 }deg,
    #ddd ${ (angle &#42; (i+1)) + 1 }deg,
    white ${ (angle &#42; (i+1)) + 1 }deg,;}
}

// Draw the grids
document.querySelectorAll('.grids>div').forEach((grid,i) => {
  grid.style.clipPath =polygon(${ vertices[0] });
});
document.querySelector('.grids:nth-of-type(1) > div').style.background =${gradient.slice(0, -1)} );

Check it out! We already have a spider web.

See the Pen Radar chart (Grid) [forked] by Preethi Sam.

Here’s what’s happening in the code:

  1. sides is the number of sides of the chart. Again, we’re working with five sides.
  2. vertices is an array that stores the coordinates of each vertex.
  3. Since we are not constructing any graphs yet — only the grid — the number of units is set to 1, and only one item is added to the percents array at percents[0]. For grid polygons, the data values are 100.
  4. gradient is a string to construct the conic-gradient() that establishes the grid lines.
  5. angle is a calculation of 360deg divided by the total number of sides.

From there, we calculate the vertices:

  1. i is an iterator that cycles through the total number of sides (i.e., 5).
  2. j is an iterator that cycles through the total number of units (i.e., 1).
  3. n is a counter that counts in increments of 2*PI (i.e., 2𝜋, 4𝜋, 6𝜋, and so on).

The x and y values of each vertex are calculated as follows, based on the geometric equations we discussed earlier. Note that we multiply 𝜋 by -1 to steer the rotation.

cos(-1 * PI/2 + n/sides) // cos(𝜋/2 + 2𝜋/sides), cos(𝜋/2 + 4𝜋/sides)...
sin(-1 * PI/2 + n/sides) // sin(𝜋/2 + 2𝜋/sides), sin(𝜋/2 + 4𝜋/sides)...

We convert the x and y values into percentages (since that is how the data points are formatted) and then place them on the chart.

let x = (round(cos(-1 * PI/2 + n/sides) * percents[j][i]) + 100) / 2;
let y = (round(sin(-1 * PI/2 + n/sides) * percents[j][i]) + 100) / 2;

We also construct the conic-gradient(), which is part of the grid. Each color stop corresponds to each vertex’s angle — at each of the angle increments, a grey (#ddd) line is drawn.

gradient += 
  `white ${ (angle * (i+1)) - 1 }deg,
   #ddd ${ (angle * (i+1)) - 1 }deg,
   #ddd ${ (angle * (i+1)) + 1 }deg,
   white ${ (angle * (i+1)) + 1 }deg,`

If we print out the computed variables after the for loop, these will be the results for the grid’s vertices and gradient:

console.log(polygon( ${vertices[0]} )); /* grid’s polygon */
// polygon(97.5% 34.5%, 79.5% 90.5%, 20.5% 90.5%, 2.5% 34.5%, 50% 0%)

console.log(gradient.slice(0, -1)); /* grid’s gradient */
// conic-gradient(white 71deg, #ddd 71deg,# ddd 73deg, white 73deg, white 143deg, #ddd 143deg, #ddd 145deg, white 145deg, white 215deg, #ddd 215deg, #ddd 217deg, white 217deg, white 287deg, #ddd 287deg, #ddd 289deg, white 289deg, white 359deg, #ddd 359deg, #ddd 361deg, white 361deg

These values are assigned to the grid’s clipPath and background, respectively, and thus the grid appears on the page.

The Graph

// Following the other variable declarations 
// Each graph's data points in the order [B, C, D... A] 
percents[1] = [100, 50, 60, 50, 90]; 
percents[2] = [100, 80, 30, 90, 40];
percents[3] = [100, 10, 60, 60, 80];

// Next to drawing grids
document.querySelectorAll('.graphs > div').forEach((graph,i) => {
  graph.style.clipPath =polygon( ${vertices[i+1]} );
});

See the Pen Radar chart (Graph) [forked] by Preethi Sam.

Now it looks like we’re getting somewhere! For each graph, we add its set of data points to the percents array after incrementing the value of units to match the number of graphs. And that’s all we need to draw graphs on the chart. Let’s turn our attention to the labels for the moment.

The Labels

// Positioning labels

// First label is always set in the top middle
let firstLabel = document.querySelector('.labels:first-of-type');
firstLabel.style.insetInlineStart =calc(50% - ${firstLabel.offsetWidth / 2}px);

// Setting labels for the rest of the vertices (data points). 
let v = Array.from(vertices[0].split(' ').splice(0, (2 * sides) - 2), (n)=> parseInt(n)); 

document.querySelectorAll('.labels:not(:first-of-type)').forEach((label, i) => {
  let width = label.offsetWidth / 2; 
  let height = label.offsetHeight;
  label.style.insetInlineStart = calc( ${ v[i&#42;2] }% + ${ v[i&#42;2] &lt; 50 ? - 3&#42;width : v[i&#42;2] == 50 ? - width: width}px );
  label.style.insetBlockStart = calc( ${ v[(i&#42;2) + 1] }% - ${ v[(i &#42; 2) + 1] == 100 ? - height: height / 2 }px );
});

The positioning of the labels is determined by three things:

  1. The coordinates of the vertices (i.e., data points) they should be next to,
  2. The width and height of their text, and
  3. Any blank space needed around the labels so they don’t overlap the chart.

All the labels are positioned relative in CSS. By adding the inset-inline-start and inset-block-start values in the script, we can reposition the labels using the values as coordinates. The first label is always set to the top-middle position. The coordinates for the rest of the labels are the same as their respective vertices, plus an offset. The offset is determined like this:

  1. x-axis/horizontal
    If the label is at the left (i.e., x is less than 50%), then it’s moved towards the left based on its width. Otherwise, it’s moved towards the right side. As such, the right or left edges of the labels, depending on which side of the chart they are on, are uniformly aligned to their vertices.
  2. y-axis/vertical
    The height of each label is fixed. There’s not much offset to add except maybe moving them down half their height. Any label at the bottom (i.e., when y is 100%), however, could use additional space above it for breathing room.

And guess what…

We’re Done!

See the Pen Radar chart (Pentagon) [forked] by Preethi Sam.

Not too shabby, right? The most complicated part, I think, is the math. But since we have that figured out, we can practically plug it into any other situation where a radar chart is needed. Need a four-point chart instead? Update the number of vertices in the script and account for fewer elements in the markup and styles.

In fact, here are two more examples showing different configurations. In each case, I’m merely increasing or decreasing the number of vertices, which the script uses to produce different sets of coordinates that help position points along the grid.

Need just three sides? All that means is two fewer coordinate sets:

See the Pen Radar chart (Triangle) [forked] by Preethi Sam.

Need seven sides? We’ll produce more coordinate sets instead:

See the Pen Radar chart (Heptagon) [forked] by Preethi Sam.

The Complex But Awesome CSS border-image Property

Category Image 052

The border-image property is nothing new. Even deprecated Internet Explorer supports it, so you know we’re treading well-charted territory. At the same time, it’s not exactly one of those properties you likely keep close at hand, and its confusing concepts of “slicing” and “outsets” don’t make it the easiest property to use.

I’m here to tell you that border-image is not only capable of producing some incredibly eye-catching UI designs but is also able to accomplish some other common patterns we often turn to other properties and approaches for.

In this article, my plan is to dispel the confusing Aspects of border-image without getting into a bunch of theoretical explanations and technical jargon. Instead, we’re going to have fun with the property, using it to create shapes and put a different spin on things like range sliders, tooltips, and title decorations.

By the end of this, I hope that border-image becomes your new favorite property just as it has become mine!

The Concept of Image Borders

There are a few specific Aspects of border-image that I think are crucial for understanding how it works.

It’s Easy To Accidentally Override A Border Image

The CSS Backgrounds and Border Module Level 3 specification says border-image should replace any regular border you define, but it’s not always the case. Try the code below, and all you will see is a red border.

/* All I see is a red border */
.element {
  border-image: linear-gradient(blue, red) 1;
  border: 5px solid red;
}

That’s because we’re technically declaring border after border-image. Order really does matter when working with border-image!

/* 👍 */
.element {
  border: 5px solid red;
  border-image: linear-gradient(blue, red) 1;
}

You can already see how this could be confusing for anyone jumping into border-image, especially for the first time. You will also notice that our gradient border has a thickness equal to 5px, which is the border-width we defined.

I make it a personal habit not to use border and border-image together because it helps me avoid overriding the border image I’m trying to create and be able to control the border decoration using only one property (even if both can be used together). So, if you get a strange result, don’t forget to check if you have a border declared somewhere.

It Is Painted Over Backgrounds And Box Shadows

The second tip I want to offer is that border-image is painted above the element’s background and box-shadow but below the element’s content. This detail is important for some of the tricks we will use in later examples. The following Pen demonstrates how a border image is applied in that order:

If we were to translate the figure above into code using the provided variables as values, it would look like this:

border-image:
  linear-gradient(...)
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

By default, border-image considers the boundaries of the element (illustrated with the blue dotted border in Figure 1) as its area to paint the gradient, but we can change this using the <outset> to increase that area and create an overflow. This is super useful to have “outside” decorations.

Then, the <width> is used to split the area into nine regions, and the <slice> is used to split the source (i.e., the gradient) into nine slices as well. From there, we assign each slice to its corresponding region. Each slice is stretched to fill its assigned region, and if they don’t share equal dimensions, the result is typically a distorted image slice. Later on, we will learn how to control that and prevent distortion.

The middle region is kept empty by default. That said, it is totally possible to use the fill keyword to do what it says and fill the middle region with slice nine (which is always the center slice).

border-image: linear-gradient(...) fill
  s-top s-right s-bottom s-left / 
  w-top w-right w-bottom w-left /
  o-top o-right o-bottom o-left;

I know this was a pretty fast primer on border-image, but I think it’s all we need to do some pretty awesome stuff. Let’s jump into the fun and start experimenting with effects.

Gradient Overlay

Our first experiment is to add a gradient overlay above an existing background. This is a fairly common pattern to improve the legibility of text by increasing the contrast between the text color and the background color.

There are several well-known approaches to setting an overlay between text and content. Here’s one from Chris Coyier back in 2013. And that isn’t even the most widely-used approach, which is likely using pseudo-elements.

But border-image gives us a one-line way to pull it off:

.overlay {
  border-image: fill 0 linear-gradient(#0003,#000); 
}

That’s all! No extra element, no pseudo-element, and no need to modify the background property.

Well, guess what? The border-image property can pull it off with one line of code:

.full-background {
  border-image: conic-gradient(pink 0 0) fill 0//0 100vw;
}

If you compare what we just did with the gradient overlay example, the <outset> is the only difference between the implementations. Other than that, we have a single slice placed in the middle region that covers the entire area we extended to the edge of the screen.

We are not limited to a solid color, of course, since we are working with gradients.

Fancy Headings

Another thing we can use border-image for is decorating headings with fancy borders. Let’s start with the exact same implementation we used for the full-width backgrounds. Only this time, we’re replacing the conic-gradient() with a linear-gradient():

.full-background {
  border-image: linear-gradient(0deg, #1095c1 5px, lightblue 0) fill 0//0 100vw;
}

Now we apply this to an <h1> element:

So, that’s two different ways to get the same effect using the same border-image syntax. We can actually get this a third way as well:

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 0 1 0/0 0 8px 0/0 100vw 0 0;
}

This time, I have defined a bottom slice equal to 1 (unitless values are computed as pixels), which produces two slices, the seventh (bottom center) and the ninth (center). From there, I have set the seventh region to a height of 8px. Note that I am not using the fill keyword this time, so the middle region is not filled like it was last time. Instead, we only fill the seventh region that takes up 100% of the boder-image’s area and 8px of its height.

You’re wondering why I am defining a slice equal to 1, right? The goal is to have only two slices: the seventh (bottom center) and the ninth (middle), and since we are applying a solid color, the size doesn't matter. That’s why I used 1; a small positive value. Any value will work (e.g., 0.5, 2, 100, 50%, 76%, and so on); it’s just that 1 is shorter to type. Remember that the slice will get stretched within its region, so 1px is enough to fill the whole region.

Here’s the deal: The slice value doesn’t really matter when working with a solid coloration. In most cases, the value winds up being 0 (empty) or 1 (filled). You can think of it as binary logic.

We could do this a fourth way!

.full-line {
  border-image: conic-gradient(#1095c1 0 0) 0 1 0 0/calc(100% - 8px) 100% 0 0/0 100vw 0 0;
}

I’ll let you do the work to figure out how the above CSS works. It’s a good opportunity to get a feel for slicing elements. Take a pen and paper and try to identify which slices we are using and which regions will be filled.

One thing that makes border-image a complex property is all the different ways to achieve the same result. You can wind up with a lot of different combinations, and when all of them produce the same result, it’s tough to form a mental model for understanding how all of the values work together.

Even though there is no single “right” way to do these heading borders, I prefer the second syntax because it allows me to simply change one color value to establish a “real” gradient instead of a solid color.

.full-line {
  border-image: repeating-linear-gradient(...) fill 0 /
    calc(100% - var(--b)) 0 0/0 100vw 0 0 repeat;
}

Let’s try another syntax for the same effect:

h2 {
  --s: 3px;   /* the thickness */
  --w: 100px; /* the width */
  --g: 10px;  /* the gap */
  border-image: 
     conic-gradient(red 0 0) 
     0 50%/calc(50% - var(--s)/2) var(--w)/0 calc(var(--w) + var(--g));
}

The top and bottom values of the <slice> are equal to 0, and the left and right ones are equal to 50%. This means that slices six and eight share the gradient. All the other slices — including the center — are empty.

As far as the regions go, the top and bottom regions (consisting of regions 1, 5, and 2 at the top and regions 4, 7, and 3 at the bottom) have a height equal to 50% - var(--s)/2 leaving the --s variable as a height for the remaining regions (6, 8, and 9). The right and the left regions have a width equal to the --w variable. Since slices 6 and 8 are the only ones that are filled, the only regions we need to care about are 6 and 8. Both have a height equal to the border’s thickness, --s, and a width equal to --w.

I think you know how the rest of the story goes.

Notice I am using 50% as a slice. It demonstrated how any value does the job, as we discussed in the last section when I explained why I chose to use a value of 1 but also to prepare for the next effect where I will be using a real gradient:

See the Pen Horizontal lines around your title with gradient coloration by Temani Afif.

When it comes to real gradients, the value of the slice is important, and sometimes you need very precise values. To be honest, this can be very tricky, and I even get lost trying to figure out the right value.

Let’s end this section with more examples of title decorations. When combined with other properties, border-image can make really nice effects.

See the Pen Fancy title divider with one element by Temani Afif.

See the Pen Fancy title divider with one element by Temani Afif.

More Examples

Now that we’ve seen several detailed examples of how border-image, I’m going to drop in several other examples. Rather than explaining them in great detail, try to explain them in your own words by inspecting the CSS, and use these as inspiration for your own work.

Infinite Image Decorations

When it comes to images, border-image can be a lifesaver since we don’t have access to pseudo-elements. Here are some cool infinite decorations where we can have a touch of 3D effect.

See the Pen Infinite image shadow by Temani Afif.

See the Pen Infinite image shadow II by Temani Afif.

See the Pen Infinite image stripes shadow by Temani Afif.

See the Pen 3D trailing shadow for images by Temani Afif.

If you check the code in these examples, you will find they share nearly the same structure. If you have trouble recognizing the pattern, please don’t hesitate to leave a comment at the end of this article, and I would be happy to point it out.

Custom Range Slider

I wrote a detailed article on how to create the following example, and you can refer to it for range slider variations using the same technique.

See the Pen CSS only custom range sliders by Temani Afif.

I used border-image and styled only the “thumb” element. Range inputs are known to have different implementation cross-browser, but the “thumb” is common between all of them.

Ribbon Shapes

In case you missed it, I have created a collection of more than 100 single-element ribbon shapes, and some of them rely on border-image. I call them the “infinite ribbons.”

See the Pen Full screen Ribbon title by Temani Afif.

See the Pen Infinite Ribbon Shapes by Temani Afif.

Heart Shapes

I have written about CSS heart shapes using different approaches, and one of them uses a border-image technique.

.heart {
  width: 200px;
  aspect-ratio: 1;
  border-image: radial-gradient(red 69%,#0000 70%) 84.5%/50%;
  clip-path: polygon(-42% 0,50% 91%, 142% 0);
}

See the Pen Heart shape using border-image by Temani Afif.

The interesting part here is the slice that is equal to 84.5%. That is a bigger value than 50%, so it may seem incorrect since the total exceeds 100%. But it’s perfectly fine because slices are able to overlap one another!

When using values bigger than 50%, the corner slices (1, 2, 3, and 4) will share common parts, but the other slices are considered empty. Logically, when using a slice equal to 100%, we will end with four slices containing the full source.

Here is an example to illustrate the trick:

See the Pen Overview of the slice effect by Temani Afif.

The slider will update the slice from 0% to 100%. On the left, you can see how the corner slices (1-4) grow. Between 0% and 50%, the result is logical and intuitive. Bigger than 50%, you start having the overlap. When reaching 100%, you can see the full circle repeated four times because each slice contains the full gradient, thanks to the overlap.

It can be confusing and not easy to visualize, but overlaps can be really useful to create custom shapes and fancy decorations.

Tooltips

What about a simple tooltip shape with only two properties? Yes, it’s possible!

See the Pen A simple Tooltip using 2 CSS properties by Temani Afif.

.tooltip {
  /* triangle dimension */
  --b: 2em; /* base */
  --h: 1em; /* height*/

  border-image: conic-gradient(#CC333F 0 0) fill 0//var(--h);
  clip-path: 
    polygon(0 100%,0 0,100% 0,100% 100%,
      calc(50% + var(--b)/2) 100%,
      50% calc(100% + var(--h)),
      calc(50% - var(--b)/2) 100%);
}
Filling Border Radius

Unlike most decorative border properties (e.g., box-shadow, outline, border, and so on), border-image doesn’t respect border-radius. The element is still a box, even if we’ve rounded off the corners. Other properties will recognize the visual boundary established by border-radius, but border-image bleeds right through it.

That could be a drawback in some instances, I suppose, but it’s also one of the quirky things about CSS that can be leveraged for other uses like creating images with inner radius:

See the Pen Inner radius to image element by Temani Afif.

Cool, right? Only one line of code makes it happen:

img {
  --c: #A7DBD8;
  --s: 10px; /* the border thickness*/

  border-image: conic-gradient(var(--c) 0 0) fill 0 // var(--s);
}

We can even leave the center empty to get a variation that simply borders the entire element:

See the Pen Rounded images inside squares by Temani Afif.

Conclusion

Did you know border-image property was such a powerful — and flexible — CSS property? Despite the challenge it takes to understand the syntax, there are ways to keep the code clean and simple. Plus, there is often more than one “right” way to get the same result. It’s a complicated and robust CSS feature.

If the concepts of slicing and defining regions with border-image are still giving you problems, don’t worry. That’s super common. It took me a lot of time to fully understand how border-image works and how to use it with different approaches to the syntax. Give yourself plenty of time, too. It helps to re-read things like this article more than once to let the concepts sink in.

Complexities aside, I hope that you will add border-image to your toolbox and create a lot of magic with it. We can do even more with border-image than what was demonstrated here. I actually experiment with this sort of stuff frequently and share my work over at my CSS Tip website. Consider subscribing (RSS) to keep up with the fun and weird things I try.

Special thanks to @SelenIT2, who pushed me to explore this property and wrote an excellent article on it.

Gradients, Blend Modes, And A Really Cool Hover Effect

Category Image 052

Do you know how box-shadow is sometimes used as a hover effect? It adds depth to something, like a button, and can create the impression that it is being pressed into the page.

Let’s pull it all together:

See the Pen Gradient Hover [forked] by Preethi Sam.

Have fun with this! Try different colors and different gradations. Here’s another example where the gradient is applied to text elements:

See the Pen Gradient Hover 2 [forked] by Preethi Sam.

Aesthetics aside, remember to use designs, layouts, and colors that make it clear to users that they are interacting with the element and what that interaction does.

Further Reading On SmashingMag

A Few Interesting Ways To Use CSS Shadows For More Than Depth

Category Image 052

The world of post-modern web design is one where the light doesn’t cast many shadows. That doesn’t mean CSS shadows are going away. On the contrary, they’ve become more adaptive. Shadows are an incredibly useful design element. We know they add depth to an otherwise two-dimensional web design, but did you know we can stack, animate, and manipulate them in ways that go beyond that?

I’ve been experimenting with shadows. In this article, I’m going to share several “tricks” I’ve discovered along the way and how they can be used to create interesting effects that have little to do with their primary role of adding depth. We’ll look at an effect that works by stacking layers of shadows that transition on hover. After that, I will show you how to make a shadow of a shadow. Lastly, we’ll play with shadows on text as an alternative to color.

Ready for some fun? Let’s start with an interesting hover effect.

The Introspective Shadow Hover Effect

Most of us are familiar with the inset keyword. It’s an optional value of the CSS box-shadow property.

When inset is specified, the shadow is cast inside the element, directed inward. It’s commonly used to make it look as if an element has been stamped into the surface of the web page. We are going to push that shadow further, both metaphorically and literally, to create an overlay hover effect for image transitions.

Just as we can control the shadow’s blur radius — how far the shadow spreads outward — we can choose to apply no blur at all to the shadow. We can combine that with the fact that inset shadows are painted over an element’s background (unlike default shadows that are cast beneath the element) to create what I call a “veil” that sits on top of an element.

Let’s start with a single div in the HTML:

<div class="item"></div>

There’s nothing to see yet. So, let’s add some dimensions, a background color, and a border radius to make a green circle.

.item {
  width: 250px;
  height: 250px;
  background: green;
  border-radius: 50%;
}

This is nothing fancy so far. I merely want to demonstrate that we can essentially cover the green background with a red inset box-shadow:

.item {
  width: 250px;
  height: 250px;
  background: green;
  border-radius: 50%;
  box-shadow: inset 250px 250px 0 red;
}

Now we have a red circle with a green background beneath it. We can remove the red inset shadow on hover to reveal the green background:

.item:hover {
  box-shadow: none;
}

See the Pen Inward Shadow Pt. 1 [forked] by Preethi Sam.

Since shadows can be layered and are supported by CSS transitions, let’s incorporate that for a more fluid design. First, I’m going to update the HTML a bit by adding a span inside the .item:

    <div class="item">
      <span>The New York Times</span>
    </div>
    <!-- more items -->

For the CSS, it’s the same idea as before. We want a circle with an inset shadow and a background:

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow: inset -300px -300px 0 black,
}

The difference so far is that I am using a background-image instead of a background-color. They are absolutely interchangeable for the hover effect we’re working on.

Next, I’m going to do two things. First, I’m going to stack more inset shadows inside the .item. Then I’m changing the text color to white, but only for a moment so the background image shows all the way through.

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow:
    inset -300px -300px 0 black,
    inset 300px -300px 0 green,
    inset -300px 300px 0 blue,
    inset 300px 300px 0 yellow,
    0 0 20px silver; /* standard outset shadow */
  color: white;
  }

Even after we add those four extra shadows, we still are left with only a black circle that says “The New York Times” on it in white. The trick is to remove those shadows on hover, change the color of the text to transparent, and reveal the logo beneath our stack of inset shadows.

.item:hover {
  box-shadow:
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    0 0 20px silver; /* retain the outset shadow */
  color: transparent;
}

That works! But perhaps we should add a little transition in there to smooth it out:

.item {
  width: 300px;
  height: 300px;
  background-image: url('nytimes.svg');
  border-radius: 50%;
  box-shadow:
    inset -300px -300px 0 black,
    inset 300px -300px 0 green,
    inset -300px 300px 0 blue,
    inset 300px 300px 0 yellow,
    0 0 20px silver; /* standard outset shadow */
  color: white;
  transition:
    box-shadow ease-in-out .6s,
    color ease-in-out .5s;
  }

.item:hover {
  box-shadow:
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    inset 0 0 0 transparent,
    0 0 20px silver; /* keeping the outset shadow */
  color: transparent;
}

The only other thing I think that’s worth calling out is that the outward shadow in the stack is not removed when the .item is hovered. I only want to remove the inset shadows.

Here’s the final result:

See the Pen Inward Shadow Pt. 2 [forked] by Preethi Sam.

I used CSS variables throughout so you can change the colors of the shadows and the size of the element.

Casting A Shadow Of A Shadow

If we learned anything from that last example, it’s that shadows are visually interesting: they can bend, fade, intersect, and transition. But what about a shadow casting another shadow? Can we create a shadow of an element’s shadow?

This is not the same as stacking layers of shadows as we did earlier. Rather, we will be making a silhouette of a shadow. And because we have a second way to add shadows to elements with the CSS drop-shadow() filter, we can do exactly that.

A drop-shadow() is a little different than a box-shadow. Where a box-shadow casts a shadow along the physical edges of the element’s bounding box, a drop-shadow() ignores the box and casts a shadow along the element’s shape.

When drop-shadow() is given to an element with a box-shadow, the shadow from the box-shadow will cast a shadow of its own. We can combine these to make interesting effects, like a Venn diagram shape.

.item {
  box-shadow: 0 0 20px black ;
  filter: drop-shadow(-30px 0 0 blue);
}

See the Pen Shadow of a Shadow Pt. 1 [forked] by Preethi Sam.

This simple combination of box and drop shadows can lead to interesting designs, like shadows that cast shadows. Let’s start with some HTML that includes the same .item element we used in the last section. This time, we’ll place two child elements inside it, another div and an img:

<div class="item">
  <div class="background"></div>
  <img src="image.jpeg" />
</div>

<!-- more items -->

The .item is merely serving as a container this time. The real work happens on the .background child element. The image is purely there for decoration. We’re going to set a box-shadow on the .background element, then add a stack of three drop-shadow() layers to it:

/* third circle in the following demo */
.item > .background {
    box-shadow: 0 0 40px rgb(255 0 0 / .5);
    filter:
      drop-shadow(-20px 0 0 rgb(255 0 0 / .5))
      drop-shadow(20px 0 0 rgb(255 0 0 / .5))
      drop-shadow(20px 0 0 rgb(255 0 0 / .5));
}

We can also use transitions with these effects (as in the middle circle below).

See the Pen Shadow of a Shadow Pt. 2 [forked] by Preethi Sam.

The Textual Shadow

The last effect we’re going to look at involves the CSS text-shadow property. It’s actually less of a complicated “trick” than it is a demonstration of using and showing just the shadow of a text element for color purposes.

Specifically, I’m talking about transparent text with a shadow on it:

/* second column in the below demo */
p {
  color: transparent;
  text-shadow: 1px 1px 0 black;
}

See the Pen Textual Shadow Pt. 2 [forked] by Preethi Sam.

Notice the emoji? Instead of the full-color deal, we normally get, this emoji is more like an icon filled with a solid color. This is one way to make a quick and dirty icon system without drawing them or working with files.

We could have also pulled this off with background-clip: text to clip around the shape of the emoji or apply a drop-shadow(). However, that affects the background, limiting where it can be used. Plus, I like the idea of using text-shadow with text elements since that’s what it’s used for, and emoji are part of the text.

You might think there’s a “gotcha” with underlines. For example, text-shadow ignores the text decoration of links.

See the Pen Text Shadow No Likey Link Underlines [forked] by Geoff Graham.

No big deal. If you need to support underlines, we can reach for the CSS text-decoration and text-underline-offset properties:

p {
  color: transparent;
  text-shadow: 1px 1px 0 black;
  text-decoration-line: underline;
  text-decoration color: black;
  text-underline-offset: 3px;
}

See the Pen Shadow-Only Link With Underline [forked] by Geoff Graham.

Conclusion

That’s a look at three interesting ways to use CSS shadows as more than that thing you use to add depth. We looked at one way that uses inset shadows to hide the contents of an element’s background for a neat hover effect. Then there was the idea of combining box-shadow and drop-shadow() to cast a shadow of another shadow. We capped things off with a quick way to manipulate text and emoji with text-shadow.

I hope these experiments give you the inspiration to do some CSS shadow experiments of your own. Shadows and gradients are perhaps the two most important CSS features for “drawing” with CSS, like many of the examples you’ll see on Lynn Fisher’s A Single Div project. Shadows have incredible browser support, so the options are plentiful as far as what we can do with them.

Further Reading On SmashingMag

How To Build A Magazine Layout With CSS Grid Areas

Category Image 052

In this article, I want to talk about the amazing possibilities of a CSS grid and how it allows for complex layouts that are closer to a print design. The design we’ll discuss is actually one I got to work on for a client (modified slightly to a demo case). It will cover two big use cases of a CSS grid:

  1. Having a static grid where we define the specified start and end points for each element;
  2. Using CSS grid template areas to reorder a simple HTML layout easily without updating the HTML.

As a bonus, we will also touch on object-fit and aspect-ratio, which come in handy as well.

Here you can see the design we will be implementing: desktop on the left and a cropped version for mobile on the right (imagine the mobile view to continue with sections 3 and 4). There is quite a lot going on here, and nothing really fits into neat rows and columns. The images are laid out on an uneven grid, sometimes even overlapping, and we have some narrow text and a numbering element that double as a design element.

Artisanal Image Layouts

Let us first look at the image grid elements inside each colored component. While we have four colored components, there are only two variants that get repeated. For easier comparison, I have cut the desktop version in half and put the two halves next to one another — this makes it easier to compare. As you can see, the first and third are the same, as are the second and fourth. If we compare just the first and second variants, they differ, but the basic building blocks are very similar (a full-sized background color, a big image block, a column with a number, and some text). Due to this, we can consider it the same component, just with two alternatives.

In the olden days, we would have had to do the image grid in Photoshop and then add it as one image to the page. Obviously, we could still do this, but that solution has never been particularly good for responsive websites, and using the picture element would work, but we would have to do several layouts in Photoshop and redo everything if we want to change a picture. We would need to do this every time this element gets added with different pictures.

But we wouldn’t be at this part in this article if there wasn’t an alternative! For a while now, it has been safe to use CSS grid, and it is able to solve this layout quite neatly with only a few lines of CSS.

CSS Grid allows us to define a formal grid definition — columns and rows — on the parent element and specify for the children where they should go within the grid. We also get the same justify and align capabilities that flex offers. This removes a lot of the need for wrapping divs and also makes the CSS slimmer.

One thing to note: As a result, your graphical layout can be different from the document structure in HTML.

Screen readers will still rely on the HTML structure, though, so put the most important information first and try to keep everything in a sensible order.

Now let’s get to our images. What is it that we need at a minimum? A container for the images and the images themselves. And you know what? With CSS Grid, that is actually enough — say goodbye to five layers of wrapping divs!

<div class="grid image-grid-3-m4">
   <img class="image-0 " src="" /> 
   <img class="image-1 " src="" />
   <img class="image-2 " src="" />
   <img class="image-3 " src="" />
</div>

A bit more on the markup. To make the styling easier, I added an index to each image (since we want to re-use them, it has to be a class, not an ID) and two classes on the surrounding div, which we will use to define the base styling: a grid utility class and a second one used for identifying the variant. For the variant with three images on the left and a fourth on the right in desktop view, I spent some time thinking about how to best solve the fourth image’s problem: do we add it to the container with the other images and try and move it to the other side, or do we move it on mobile, and so on. In the end, I decided to add the fourth image in the container for the images but hide it on the desktop via CSS and have a separate div in a second location with the same image for displaying in the desktop version. Using display:none will also hide that version from screen readers.

Now that we have the basic HTML and our images in place, it is time to focus on the CSS. If you are completely new to a CSS grid, this helpful article goes into the full syntax and explains it in detail. Unfortunately, I cannot describe the full syntax in this article.

First off, we need to define our grid. Since I had a design to work with, I used a tool that allowed me to put lines on top of the image, position one line at each edge of the image, and see the pixel dimensions in between. It would have been great if the designer had already used a formal grid and told me about it, but unfortunately, that was not the case, so I used the proportionate dimensions as an approximation of what I should use in the grid. Basically, I asked myself what the smallest common divisible number was for each — with some wiggle room — and used that. (Welcome to math class.) My goal was to have the same size for all grid columns and rows while being flexible on the number of columns or rows.

With this method, I determined that I wanted to have 14 columns on mobile and 7 rows plus some uniform gaps. This allowed me to approximate the distribution in the layout while keeping the Aspect ratios close to what they had envisioned. Based on that, we get the following CSS:

.grid {
  display: grid;
}

.image-grid-3-m4 {
  grid-template-rows: repeat(7, 1fr);
  grid-template-columns: repeat(14, 1fr);
    gap: 0.5rem;
}

With these four lines of CSS, we have a grid that is ready to be filled. If you follow along, you will notice that the images are now filling one grid cell each in the first row. This is the automatic layout mechanism that the browser uses, and depending on what you want to do, it can be ideal for defining an evenly laid-out design in seconds.

What is the minimum HTML for that? Again we do not need a lot of extras; a surrounding div and then one for each part is all that we need:

<div class="container">
  <div class="images"></div>
  <div class="numbering"></div>
  <div class="text"></div>
  <div class="single-image"></div>
</div>

For our example, all areas have a class name that identifies what they will hold later. If we look at the mobile layout, though, the number is on top of the images! The cool thing about a CSS grid is that you can layer elements. We already used that for the images above. As you can see, two of them overlap, and we can also do that for whole areas. The layering will be controlled by our old friend, the z-index. Same rules as always: the higher z-index wins and comes to the front.

With that in mind, let’s create two areas: one at the top with the images filling the area and the number as a top layer with most of it transparent to show the images, and the second area below for the text. We could use the grid columns and rows syntax we used earlier, but in this case, we can make our lives even easier with grid-template-areas. With this, you can add names to parts of your grid and then decide for each element which grid area it should appear in. Especially for the page or component frame, this is a much easier and faster way to work and be able to read it all again later than using all of the non-descript numbers.

I think this will be easier to understand with an example.

.container {  
  display: grid;
  grid-template-columns: 1fr;
  grid-template-rows: repeat(2, 1fr);
  gap: 1.2rem;
  grid-auto-flow: row;
  grid-template-areas:
    "numerology"
    "text";
}

.images { grid-area: numerology; }

.numbering { grid-area: numerology; }

.text { grid-area: text; }
.single-image {display:none}

We define the container as a grid once again, add two rows to it, and then use ‘grid-template-areas’ to give these rows a name. The syntax for this is very unusual for CSS, but it gives you a mini-view of your layout: Numerology is the name of the cell in the first row and Text in the second.

With those rows now having names, we can easily position our elements. Both the images and the container for the number go into the first row, and the text will go to the second row. So in the example above, we add the grid-area to the CSS for the class we applied to the div in the HTML. With those few lines, we have defined the layout.

To achieve the overlay effect for the number, the white box will be in its container and gets a fixed width and height. We can then use flex to center it in the container.

But how do we get from this to the desktop version, you may ask? Pretty easily, actually! For the overall design of the website, we are already using a 14-column grid on the desktop for all elements. If we overlay the design with some grid markers, we see the widths everything should take approximately.

Obviously, our named areas from the mobile view will not really help us for this version, but we can simply update them in a media query for our desktop view and also define different area names:

.container {  display: grid;
  grid-template-columns: repeat(14, minmax(0, 1fr));
  grid-template-rows: repeat(2, 1fr);
  gap: 1.2rem;
  grid-auto-flow: row;
  grid-template-areas:
    "images images images images images . numbering numbering numbering single-image single-image single-image single-image single-image"
    "images images images images images . text text text single-image single-image single-image single-image single-image";
}

.images { grid-area: images; }

.numbering { grid-area: numbering; }

.text { grid-area: text; }

.single-image { grid-area: single-image; }

Let me be the first one to say that, yes, this is really not a pretty way of defining this, but unfortunately, the template-areas syntax does not include the repeat keyword as the column definition does.

But take a moment and have a closer look. What you can see is that we define the first five columns to belong to the images name in both rows, then we have a period, which means nothing goes here, then we have three columns for the numbering in row one and three for the text in row two, and at the end, five columns for single-image. Personally, I like to use an online generator that allows me to visually define these areas and copy the needed CSS.

Now with just under 20 lines of code, we have completely changed the layout without having touched the very simple HTML structure at all! But what about the alternative version for 2 and 4? They only use a slightly different layout, so why not add some classes for .version-a and .version-b on the container and have the grid-template-areas defined by that on the desktop? It is that simple. Look at the following:

  .version-1 {
      grid-template-areas:
    "images images images images images . numbering numbering numbering single-image single-image single-image single-image single-image"
    "images images images images images . text text text single-image single-image single-image single-image single-image";
  }
.version-1 .single-image {
  grid-area: single-image;
  display:block;
 }
.version-2 { grid-template-areas: ". numbering numbering numbering . . images images images images images images images images" ". text text text . . images images images images images images images images"; }

To me, this is still pretty crazy, to be honest. For the longest time, a layout like this would have been completely out of reach or very complicated to make and having shared HTML between versions 1 and 2 would have been near impossible, at least for the complete HTML. Now we can just wave a magic wand and update where it should show up. Pretty heady stuff.

Another practical example where this helped me a lot was defining areas for a product detail page on an e-commerce website. Being able to move elements around to where they make sense in different contexts is amazing, but it also means that you need to adjust your mental model a bit to how HTML and CSS are connected. And this is even just the beginning. With container queries and layers, there is much on the horizon that will open up a lot more possibilities in the future, and I am totally here for it!

Finally, here is the full version of the design with everything pulled together:

See the Pen Untitled [forked] by Pfenya.

Additionally, if you are curious about the final live webpage, it can be found here.

State Of CSS Survey: Influence The Future Of CSS

Category Image 052

This year, I joined the team and helped design the survey together with the community which led to a number of improvements. If you write CSS frequently, investing a few minutes to fill it in could come back to you hundredfold, since implementers make decisions on what to work on based on the developer pain points identified through the survey every year. In fact, Chrome is funding work on the survey for this very reason.

Past Surveys

So, how did past surveys help web developers? Let’s look at the impact in Chrome, as described to us by Nicole Sullivan, Product Manager for Chrome at Google:

“I showed the ‘Missing features’ section to my team before the pandemic and we got to work on it. Several things on that list are underway.”

Indeed, literally everything in that list is now being worked on or finished unless there was no (stable) specification for it:

  • Container queries
    Size queries have shipped in Chrome 106 , style queries behind a flag.
  • Parent selector/:has selector
    Shipped in Chrome 105.
  • Nesting
    Currently underway, delayed a bit due to discussions in the CSS Working Group about last minute changes to the syntax.
  • 🟡 Functions
    No specification to implement yet, but is being worked on in the CSS WG.
  • Scoping
    Experimental implementation in Chrome 105 behind a flag.
  • 🟡 Mixins
    No specification to implement yet, but ideas are being explored in the CSS WG.
  • Subgrid
    Implementation underway.

Let’s look at the corresponding section in the 2020 results. A lot of overlap, but some additional items:

The 2021 corresponding section includes roughly the same items, with one new thing: color functions. And lo and behold, the color functions for which there is a stable specification are being implemented in Chrome as we speak, and Chrome has funded specification work on the rest.

And it’s not just Chrome. The focus of Interop 2022 was largely shaped by these results.

What’s Next?

We’re taking on the world of styles and selectors to try and identify upcoming trends, and figure out what featurs and tools to learn next. What’s more, the survey results will also help browser vendors prioritize their roadmaps and work towards better compatibility between browsers.

What do you want to see more of in CSS? Better typography? New responsive layout features? New features to improve maintainability? Layout? Components? Something else? The sky is the limit! Make sure to share your CSS dreams with us in the survey, and they may well start coming true.

Easy Fluid Typography With clamp() Using Sass Functions

Typography Definitions Cover

Fluid typography is getting a lot more popular, especially since the clamp() math function is available in every evergreen browser. But if we’re honest, it’s still a lot of mathematics to achieve this. You can use tools such as utopia.fyi, which are fantastic. But in large projects, it can get messy pretty fast. I’m a big fan of readable and maintainable code and always want to see what my code is doing at a glance. I’m sure there are many more of you like that, so instead of adding a full clamp() function inside of our code, maybe we can make this a bit more readable with Sass.

Why Should We Use Fluid Typography?

Usually, when designing for different screen sizes, we use media queries to determine the font size of our typographic elements. Although this usually gives enough control for the more conventional devices, it doesn’t cover all of the screen sizes.

By using fluid typography, we can make the typography scale more logically between all sorts of different devices.

This is now possible in all evergreen browsers because of the clamp() function in CSS. It is perfect for the job and reduces our media query writing, thus saving us a bit of file size along the way.

How Exactly Does This clamp() Function Work For Typography?

In short, the clamp function looks like this:

clamp([min-bound], [value-preferred], [max-bound]);

This takes into account three numbers: a minimum bound, preferred value, and a maximum bound. By using rem values, we can increase the accessibility a bit, but it’s still not 100% foolproof, especially for external browser tools.

If you want a more in-depth explanation of the math, I suggest you read this post from Adrian Bece “Modern Fluid Typography Using CSS Clamp ”.

However, there is a bit of a problem. When you read those clamp functions inside your CSS, it’s still hard to see exactly what is happening. Just imagine a file full of font sizes that look like this:

clamp(1.44rem, 3.44vw + 0.75rem, 2.81rem)

But with a little help from the sass function, we can make these font sizes much more readable.

What Do We Want To Achieve With This Simple Sass Function?

In short, we want to do something like this: We have a minimum font size, from the moment our breakpoint is larger than 400px, we want it to scale it to our biggest font size until the maximum breakpoint is reached.

The minimum and maximum font sizes are covered quite easily. If we want a minimum font size of 16px (or 1rem) and a maximum font size of 32px (or 2rem), we already have the two parts of our clamp function:

clamp(1rem, [?], 2rem)
Creating A Basic Automated Fluid Function

This is where things get tricky, and I suggest you follow the article by Adrian Bece, who gives a great in-depth explanation of the math behind this.

In short, the equation is the following:

(maximum font-size - minimum font-size) / (maximum breakpoint - minimum breakpoint)

Let’s get ready to do some mathematics for this to happen in Sass, so let’s create our fluid-typography.scss function file and start by adding sass:math and the function with the values we’ll need:

@use "sass:math";

@function fluid($min-size, $max-size, $min-breakpoint, $max-breakpoint, $unit: vw) {

}

Now, let’s add the calculation for the slope inside of our function with some sass:math:

@function fluid($min-size, $max-size, $min-breakpoint, $max-breakpoint, $unit: vw) {
 $slope: math.div($max-size - $min-size, $max-breakpoint - $min-breakpoint);
}

To get a value we can work with, we’ll need to multiply our slope by 100:

$slope-to-unit: $slope * 100;

All that is left is for us to find our intercept to build the equation. We can do this with the following function:

$intercept: $min-size - $slope * $min-breakpoint;

And finally, return our function:

@return clamp(#{$min-size}, #{$slope-to-unit}#{$unit} + #{$intercept}, #{$max-size});

If we call the created sass function in our scss, we should now get fluid typography:

h1 {
   font-size: #{fluid(1rem, 2rem, 25rem, 62.5rem)}
}

A Note About Units

In most cases, we will be using a viewport width when it comes to fluid typography, so this makes a good default. However, there are some cases, especially when using the clamp() function for vertical spacing, where you want to use a viewport height instead of width. When this is desired, we can change the outputted unit and use a minimum and maximum breakpoint for the height:

h1 {
   font-size: #{fluid(1rem, 2rem, 25rem, 62.5rem, vh)}
}
Updating The Function To Make The Calculations Feel More Natural

We got what we need, but let’s be honest, most of the time, we are implementing a design, and it doesn’t feel natural to pass our viewports as rems. So, let’s update this function to use pixels as a viewport measurement. While we’re at it, let’s update the font sizes so we can use pixel values for everything. We will still convert them to rem units since those are better for accessibility.

First, we’ll need an extra function to calculate our rem values based on a pixel input.

Note: This won’t work if you change your base rem value.

@function px-to-rem($px) {
    $rems: math.div($px, 16px) * 1rem;
    @return $rems;
}

Now we can update our fluid function to output rem values even though it gets pixels as input. This is the updated version:

@function fluid($min-size, $max-size, $min-breakpoint, $max-breakpoint, $unit: vw) {
    $slope: math.div($max-size - $min-size, $max-breakpoint - $min-breakpoint);
    $slope-to-unit: $slope * 100;
    $intercept-rem: px-to-rem($min-size - $slope * $min-breakpoint);
    $min-size-rem: px-to-rem($min-size);
    $max-size-rem: px-to-rem($max-size);
    @return clamp(#{$min-size-rem}, #{$slope-to-unit}#{$unit} + #{$intercept-rem}, #{$max-size-rem});
}

Now we can use the following input:

font-size: #{fluid(16px, 32px, 320px, 960px)}

This will result in the following:

font-size: clamp(1rem, 2.5vw + 0.5rem, 2rem);

At first glance, this seems perfect, but mostly that’s because I’ve been using very simple values. For example, when clamping to a maximum value of 31px instead of 32px, our rem values won’t be so rounded, and our output will get a bit messy.

Input:

font-size: #{fluid(16px, 31px, 320px, 960px)}

Output:

font-size: clamp(1rem, 2.34375vw + 0.53125rem, 1.9375rem);

If you’re like me and find this a bit messy as well, we could round our values a little bit to increase readability and save some bytes in our final CSS file. Also, it might get a bit tedious if we always have to add the viewport, so why not add some defaults in our function?

Rounding Our Values And Adding Some Defaults

Let’s start by adding a rounding function to our Sass file. This will take any input and round it to a specific amount of decimals:

@function round($number, $decimals: 0) {
    $n: 1;
    @if $decimals > 0 {
        @for $i from 1 through $decimals {
            $n: $n * 10;
        }
    }
    @return math.div(math.round($number * $n), $n);
}

Now we can update our output values with rounded numbers. Update the function accordingly. I would suggest setting at least two decimals for the output values for the most consistent results:

@function fluid($min-size, $max-size, $min-breakpoint, $max-breakpoint, $unit: vw) {
    $slope: math.div($max-size - $min-size, $max-breakpoint - $min-breakpoint);
    $slope-to-unit: round($slope * 100, 2);
    $intercept-rem: round(px-to-rem($min-size - $slope * $min-breakpoint), 2);
    $min-size-rem: round(px-to-rem($min-size), 2);
    $max-size-rem: round(px-to-rem($max-size), 2);
    @return clamp(#{$min-size-rem}, #{$slope-to-unit}#{$unit} + #{$intercept-rem}, #{$max-size-rem});
}

Now the same example as before will give us a much cleaner result.

Input:

font-size: #{fluid(16px, 31px, 320px, 960px)};

Output:

font-size: clamp(1rem, 2.34vw + 0.53rem, 1.94rem);

Adding A Default Breakpoint

If you don’t feel like repeating yourself, you can always set a default breakpoint to your function. Try updating the function like this:

$default-min-bp: 320px;
$default-max-bp: 960px;

@function fluid($min-size, $max-size, $min-breakpoint: $default-min-bp, $max-breakpoint: $default-max-bp, $unit: vw) {
    // ...
}

Now, we don’t need to repeat these viewports all the time. We can still add a custom breakpoint but a simple input such as:

font-size: #{fluid(16px, 31px)};

Will still result in:

font-size: clamp(1rem, 2.34vw + 0.53rem, 1.94rem);

Here is the full function:

@use 'sass:math';

$default-min-bp: 320px;
$default-max-bp: 960px;

@function round($number, $decimals: 0) {
    $n: 1;
    @if $decimals > 0 {
        @for $i from 1 through $decimals {
            $n: $n * 10;
        }
    }
    @return math.div(math.round($number * $n), $n);
}

@function px-to-rem($px) {
    $rems: math.div($px, 16px) * 1rem;
    @return $rems;
}

@function fluid($min-size, $max-size, $min-breakpoint: $default-min-bp, $max-breakpoint: $default-max-bp, $unit: vw) {
    $slope: math.div($max-size - $min-size, $max-breakpoint - $min-breakpoint);
    $slope-to-unit: round($slope * 100, 2);
    $intercept-rem: round(px-to-rem($min-size - $slope * $min-breakpoint), 2);
    $min-size-rem: round(px-to-rem($min-size), 2);
    $max-size-rem: round(px-to-rem($max-size), 2);
    @return clamp(#{$min-size-rem}, #{$slope-to-unit}#{$unit} + #{$intercept-rem}, #{$max-size-rem});
}
A Final Note: Be A Happy Clamper For All users Out There

If you followed this little tutorial and were amazed by it, you might want to add this clamp() method for everything, but there is an important side note when it comes to accessibility.

Note: When you use vw units or limit how large text can get with clamp(), there is a chance a user may be unable to scale the text to 200% of its original size.

If that happens, it is WCAG failure. As Adrian Bece mentioned, it’s not 100% foolproof. Adrian Roselli has written some examples on this, which you might find interesting.

We can use this method today because of the great browser support. By being smart on the usage, I’m sure it can be a beautiful addition to your upcoming project or as an upgrade to a previous one.