Tag: Control

Don’t Fight the Cascade, Control It!

If you’re disciplined and make use of the inheritance that the CSS cascade provides, you’ll end up writing less CSS. But because our styles often comes from all kinds of sources — and can be a pain to structure and maintain—the cascade can be a source of frustration, and the reason we end up with more CSS than necessary.

Some years ago, Harry Roberts came up with ITCSS and it’s a clever way of structuring CSS.

Mixed with BEM, ITCSS has become a popular way that people write and organize CSS.

However, even with ITCSS and BEM, there are still times where we still struggle with the cascade. For example, I’m sure you’ve had to @import external CSS components at a specific location to prevent breaking things, or reach for the dreaded !important at some point in time.

Recently, some new tools were added to our CSS toolbox, and they allow us to finally control the cascade. Let’s look at them.

O cascade, :where art thou?

Using the :where pseudo-selector allows us to remove specificity to “just after the user-agent default styles,” no matter where or when the CSS is loaded into the document. That means the specificity of the whole thing is literally zero — totally wiped out. This is handy for generic components, which we’ll look into in a moment.

First, imagine some generic <table> styles, using :where:

:where(table) {   background-color: tan; }

Now, if you add some other table styles before the :where selector, like this:

table {   background-color: hotpink; }  :where(table) {   background-color: tan; }

…the table background becomes hotpink, even though the table selector is specified before the :where selector in the cascade. That’s the beauty of :where, and why it’s already being used for CSS resets.

:where has a sibling, which has almost the exact opposite effect: the :is selector.

The specificity of the :is() pseudo-class is replaced by the specificity of its most specific argument. Thus, a selector written with :is() does not necessarily have equivalent specificity to the equivalent selector written without :is(). Selectors Level 4 specification

Expanding on our previous example:

:is(table) {   --tbl-bgc: orange; } table {   --tbl-bgc: tan; } :where(table) {   --tbl-bgc: hotpink;   background-color: var(--tbl-bgc); }

The <table class="c-tbl"> background color will be tan because the specificity of :is is less specific than table.

However, if we were to change it to this:

:is(table, .c-tbl) {   --tbl-bgc: orange; }

…the background color will be orange, since :is has the weight of it’s heaviest selector, which is .c-tbl.

Example: A configurable table component

Now, let’s see how we can use :where in our components. We’ll be building a table component, starting with the HTML:

Let’s wrap .c-tbl in a :where-selector and, just for fun, add rounded corners to the table. That means we need border-collapse: separate, as we can’t use border-radius on table cells when the table is using border-collapse: collapse:

:where(.c-tbl) {   border-collapse: separate;   border-spacing: 0;   table-layout: auto;   width: 99.9%; }

The cells use different styling for the <thead> and <tbody>-cells:

:where(.c-tbl thead th) {   background-color: hsl(200, 60%, 40%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 0;   border-inline-start-width: 0;   color: hsl(200, 60%, 99%);   padding-block: 1.25ch;   padding-inline: 2ch;   text-transform: uppercase; } :where(.c-tbl tbody td) {   background-color: #FFF;   border-color: hsl(200, 60%, 80%);   border-style: solid;   border-block-start-width: 0;   border-inline-end-width: 1px;   border-block-end-width: 1px;   border-inline-start-width: 0;   padding-block: 1.25ch;   padding-inline: 2ch; }

And, because of our rounded corners and the missing border-collapse: collapse, we need to add some extra styles, specifically for the table borders and a hover state on the cells:

:where(.c-tbl tr td:first-of-type) {   border-inline-start-width: 1px; } :where(.c-tbl tr th:last-of-type) {   border-inline-color: hsl(200, 60%, 40%); } :where(.c-tbl tr th:first-of-type) {   border-inline-start-color: hsl(200, 60%, 40%); } :where(.c-tbl thead th:first-of-type) {   border-start-start-radius: 0.5rem; } :where(.c-tbl thead th:last-of-type) {   border-start-end-radius: 0.5rem; } :where(.c-tbl tbody tr:last-of-type td:first-of-type) {   border-end-start-radius: 0.5rem; } :where(.c-tbl tr:last-of-type td:last-of-type) {   border-end-end-radius: 0.5rem; } /* hover */ @media (hover: hover) {   :where(.c-tbl) tr:hover td {     background-color: hsl(200, 60%, 95%);   } }

Now we can create variations of our table component by injecting other styles before or after our generic styles (courtesy of the specificity-stripping powers of :where), either by overwriting the .c-tbl element or by adding a BEM-style modifier-class (e.g. c-tbl--purple):

<table class="c-tbl c-tbl--purple">
.c-tbl--purple th {   background-color: hsl(330, 50%, 40%) } .c-tbl--purple td {   border-color: hsl(330, 40%, 80%); } .c-tbl--purple tr th:last-of-type {   border-inline-color: hsl(330, 50%, 40%); } .c-tbl--purple tr th:first-of-type {   border-inline-start-color: hsl(330, 50%, 40%); }

Cool! But notice how we keep repeating colors? And what if we want to change the border-radius or the border-width? That would end up with a lot of repeated CSS.

Let’s move all of these to CSS custom properties and, while we’re at it, we can move all configurable properties to the top of the component’s “scope“ — which is the table element itself — so we can easily play around with them later.

CSS Custom Properties

I’m going to switch things up in the HTML and use a data-component attribute on the table element that can be targeted for styling.

<table data-component="table" id="table">

That data-component will hold the generic styles that we can use on any instance of the component, i.e. the styles the table needs no matter what color variation we apply. The styles for a specific table component instance will be contained in a regular class, using custom properties from the generic component.

[data-component="table"] {   /* Styles needed for all table variations */ } .c-tbl--purple {   /* Styles for the purple variation */ }

If we place all the generic styles in a data-attribute, we can use whatever naming convention we want. This way, we don’t have to worry if your boss insists on naming the table’s classes something like .BIGCORP__TABLE, .table-component or something else.

In the generic component, each CSS property points to a custom property. Properties, that have to work on child-elements, like border-color, are specified at the root of the generic component:

:where([data-component="table"]) {   /* These will will be used multiple times, and in other selectors */   --tbl-hue: 200;   --tbl-sat: 50%;   --tbl-bdc: hsl(var(--tbl-hue), var(--tbl-sat), 80%); }  /* Here, it's used on a child-node: */ :where([data-component="table"] td) {   border-color: var(--tbl-bdc); }

For other properties, decide whether it should have a static value, or be configurable with its own custom property. If you’re using custom properties, remember to define a default value that the table can fall back to in the event that a variation class is missing.

:where([data-component="table"]) {   /* These are optional, with fallbacks */   background-color: var(--tbl-bgc, transparent);   border-collapse: var(--tbl-bdcl, separate); }

If you’re wondering how I’m naming the custom properties, I’m using a component-prefix (e.g. --tbl) followed by an Emmett-abbreviation (e.g. -bgc). In this case, --tbl is the component-prefix, -bgc is the background color, and -bdcl is the border collapse. So, for example, --tbl-bgc is the table component’s background color. I only use this naming convention when working with component properties, as opposed to global properties which I tend to keep more general.

Now, if we open up DevTools, we can play around with the custom properties. For example, We can change --tbl-hue to a different hue value in the HSL color, set --tbl-bdrs: 0 to remove border-radius, and so on.

A :where CSS rule set showing the custom properties of the table showing how the cascade’s specificity scan be used in context.

When working with your own components, this is the point in time you’ll discover which parameters (i.e. the custom property values) the component needs to make things look just right.

We can also use custom properties to control column alignment and width:

:where[data-component="table"] tr > *:nth-of-type(1)) {   text-align: var(--ca1, initial);   width: var(--cw1, initial);   /* repeat for column 2 and 3, or use a SCSS-loop ... */ }

In DevTools, select the table and add these to the element.styles selector:

element.style {   --ca2: center; /* Align second column center */   --ca3: right; /* Align third column right */ }

Now, let’s create our specific component styles, using a regular class, .c-tbl (which stands for “component-table” in BEM parlance). Let’s toss that class in the table markup.

<table class="c-tbl" data-component="table" id="table">

Now, let’s change the --tbl-hue value in the CSS just to see how this works before we start messing around with all of the property values:

.c-tbl {   --tbl-hue: 330; }

Notice, that we only need to update properties rather than writing entirely new CSS! Changing one little property updates the table’s color — no new classes or overriding properties lower in the cascade.

Notice how the border colors change as well. That’s because all the colors in the table inherit from the --tbl-hue variable

We can write a more complex selector, but still update a single property, to get something like zebra-striping:

.c-tbl tr:nth-child(even) td {   --tbl-td-bgc: hsl(var(--tbl-hue), var(--tbl-sat), 95%); }

And remember: It doesn’t matter where you load the class. Because our generic styles are using :where, the specificity is wiped out, and any custom styles for a specific variation will be applied no matter where they are used. That’s the beauty of using :where to take control of the cascade!

And best of all, we can create all kinds of table components from the generic styles with a few lines of CSS.

Purple table with zebra-striped columns
Light table with a “noinlineborder” parameter… which we’ll cover next

Adding parameters with another data-attribute

So far, so good! The generic table component is very simple. But what if it requires something more akin to real parameters? Perhaps for things like:

  • zebra-striped rows and columns
  • a sticky header and sticky column
  • hover-state options, such as hover row, hover cell, hover column

We could simply add BEM-style modifier classes, but we can actually accomplish it more efficiently by adding another data-attribute to the mix. Perhaps a data-param that holds the parameters like this:

<table data-component="table" data-param="zebrarow stickyrow">

Then, in our CSS, we can use an attribute-selector to match a whole word in a list of parameters. For example, zebra-striped rows:

[data-component="table"][data-param~="zebrarow"] tr:nth-child(even) td {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Or zebra-striping columns:

[data-component="table"][data-param~="zebracol"] td:nth-of-type(odd) {   --tbl-td-bgc: var(--tbl-zebra-bgc); }

Let’s go nuts and make both the table header and the first column sticky:

 [data-component="table"][data-param~="stickycol"] thead tr th:first-child,[data-component="table"][data-param~="stickycol"] tbody tr td:first-child {   --tbl-td-bgc: var(--tbl-zebra-bgc);   inset-inline-start: 0;   position: sticky; } [data-component="table"][data-param~="stickyrow"] thead th {   inset-block-start: -1px;   position: sticky; }

Here’s a demo that allows you to change one parameter at a time:

The default light theme in the demo is this:

.c-tbl--light {   --tbl-bdrs: 0;   --tbl-sat: 15%;   --tbl-th-bgc: #eee;   --tbl-th-bdc: #eee;   --tbl-th-c: #555;   --tbl-th-tt: normal; }

…where data-param is set to noinlineborder which corresponds to these styles:

[data-param~="noinlineborder"] thead tr > th {   border-block-start-width: 0;   border-inline-end-width: 0;   border-block-end-width: var(--tbl-bdw);   border-inline-start-width: 0; }

I know my data-attribute way of styling and configuring generic components is very opinionated. That’s just how I roll, so please feel free to stick with whatever method you’re most comfortable working with, whether it’s a BEM modifier class or something else.

The bottom line is this: embrace :where and :is and the cascade-controlling powers they provide. And, if possible, construct the CSS in such a way that you wind up writing as little new CSS as possible when creating new component variations!

Cascade Layers

The last cascade-busting tool I want to look at is “Cascade Layers.” At the time of this writing, it’s an experimental feature defined in the CSS Cascading and Inheritance Level 5 specification that you can access in Safari or Chrome by enabling the #enable-cascade-layers flag.

Bramus Van Damme sums up the concept nicely:

The true power of Cascade Layers comes from its unique position in the Cascade: before Selector Specificity and Order Of Appearance. Because of that we don’t need to worry about the Selector Specificity of the CSS that is used in other Layers, nor about the order in which we load CSS into these Layers — something that will come in very handy for larger teams or when loading in third-party CSS.

Perhaps even nicer is his illustration showing where Cascade Layers fall in the cascade:

Credit: Bramus Van Damme

At the beginning of this article, I mentioned ITCSS — a way of taming the cascade by specifying the load-order of generic styles, components etc. Cascade Layers allow us to inject a stylesheet at a given location. So a simplified version of this structure in Cascade Layers looks like this:

@layer generic, components;

With this single line, we’ve decided the order of our layers. First come the generic styles, followed by the component-specific ones.

Let’s pretend that we’re loading our generic styles somewhere much later than our component styles:

@layer components {   body {     background-color: lightseagreen;   } }  /* MUCH, much later... */  @layer generic {    body {     background-color: tomato;   } }

The background-color will be lightseagreen because our component styles layer is set after the generic styles layer. So, the styles in the components layer “win” even if they are written before the generic layer styles.

Again, just another tool for controlling how the CSS cascade applies styles, allowing us more flexibility to organize things logically rather than wrestling with specificity.

Now you’re in control!

The whole point here is that the CSS cascade is becoming a lot easier to wrangle, thanks to new features. We saw how the :where and :is pseudo-selectors allows us to control specificity, either by stripping out the specificity of an entire ruleset or taking on the specificity of the most specific argument, respectively. Then we used CSS Custom Properties to override styles without writing a new class to override another. From there, we took a slight detour down data-attribute lane to help us add more flexibility to create component variations merely by adding arguments to the HTML. And, finally, we poked at Cascade Layers which should prove handy for specifying the loading order or styles using @layer.

If you leave with only one takeaway from this article, I hope it’s that the CSS cascade is no longer the enemy it’s often made to be. We are gaining the tools to stop fighting it and start leaning into even more.


Header photo by Stephen Leonardi on Unsplash


Don’t Fight the Cascade, Control It! originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , ,

Control Layout in a Multi-Directional Website

Many business websites need a multilingual setup. As with anything development-related, implementing one in an easy, efficient, and maintainable way is desirable. Designing and developing to be ready for multiple languages, whether it happens right at launch or is expected to happen at any point in the future, is smart.

Changing the language and content is the easy part. But when you do that, sometimes the language you are changing to has a different direction. For example, text (and thus layout) in English flows left-to-right while text (and thus layout) in Arabic goes right-to-left.

In this article, I want to build a multilingual landing page and share some CSS techniques that make this process easier. Hopefully the next time you’ll need to do the same thing, you’ll have some implementation techniques to draw from.

We’ll cover six major points. I believe that the first five are straightforward. The sixth includes multiple options that you need to think about first.

1. Start with the HTML markup

The lang and dir attributes will define the page’s language and direction.

<!-- English, left-to-right --> <html lang="en" dir="ltr">  <!-- Arabic, right-to-left --> <html lang="ar" dir="rtl">

Then we can use these attributes in selectors to do the the styling. lang and dir attributes are on the HTML tag or a specific element in which the language varies from the rest of the page. Those attributes help improve the website’s SEO by showing the website in the right language for users who search for it in case that each language has a separate HTML document.

Also, we need to ensure that the charset meta tag is included and its value is UTF-8 since it’s the only valid encoding for HTML documents which also supports all languages.

<meta charset="utf-8">

I’ve prepared a landing page in three different languages for demonstration purposes. It includes the HTML, CSS, and JavaScript we need.

2. CSS Custom Properties are your friend

Changing the direction may lead to inverting some properties. So, if you used the CSS property left in a left-to-right layout, you probably need right in the right-to-left layout, and so on. And changing the language may lead to changing font families, font sizes, etc.

These multiple changes may cause unclean and difficult to maintain code. Instead, we can assign the value to a custom property, then change the value when needed. This is also great for responsiveness and other things that might need a toggle, like dark mode. We can change the font-size, margin, padding, colors, etc., in the blink of an eye, where the values then cascade to wherever needed.

Here are some of the CSS custom properties that we are using in this example:

html {   /* colors */   --dark-color: #161616;   --light-color: #eee;   --primary-text-color: var(--dark-color);   --primary-bg-color: #fff;   --shadow-color: var(--light-color);   --hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);    /* font sizes */   --logo-font-size: 2rem;   --lang-switcher-font-size: 1.02em;   --offers-item-after-font-size: 1.5rem;    /* margin and padding */   --btn-padding: 7px;   --sec-padding-block: 120px;    /* height and width */   --hero-height: 500px;   --cta-img-width: 45.75%; }

While styling our page, we may add/change some of these custom properties, and that is entirely natural. Although this article is about multi-directional websites, here’s a quick example that shows how we can re-assign custom property values by having one set of values on the <body>, then another set when the <body> contains a .dark class:

body {   background-color: var(--primary-bg-color);   color: var(--primary-text-color); } body.dark {   --primary-bg-color: #0f0f0f;   --primary-text-color: var(--light-color);    /* other changes */   --shadow-color: #13151a;   --hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313); }

That’s the general idea. We’re going to use custom properties in the same sort of way, though for changing language directions.

3) CSS pseudo-classes and selectors

CSS has a few features that help with writing directions. The following two pseudo-classes and attribute are good examples that we can put to use in this example.

The :lang() pseudo-class

We can use :lang() pseudo-class to target specific languages and apply CSS property values to them individually, or together. For example, in this example, we can change the font size when the :lang pseudo-class switches to either Arabic or Japanese:

html:lang(ar), html:lang(jp){   --offers-item-after-font-size: 1.2rem;  }

Once we do that, we also need to change the writing-mode property from its horizontal left-to-right default direction to vertical right-to-left direction account:

html:lang(jp) .about__text {   writing-mode: vertical-rl; }

The :attr() pseudo-class

The :attr() pseudo-class helps makes the “content” of the pseudo-elements like ::before or ::after “dynamic” in a sense, where we can drop the dir HTML attribute into the CSS content property using the attr() function. That way, the value of dir determines what we’re selecting and styling.

<div dir="ltr"></div> <div dir="rtl"></div>
div::after {   content: attr(dir); }

The power is the ability to use any custom data attribute. Here, we’re using a custom data-name attribute whose value is used in the CSS:

<div data-name="English content" dir="ltr"></div> <div data-name="محتوى عربي" dir="rtl"></div>
div::after {   content: attr(data-name); }

This makes it relatively easy to change the content after switching that language without changing the style. But, back to our design. The three-up grid of cards has a yellow “special” or “best” off mark beside an image.

This is the HTML for each card:

<div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">   <figure class="offers__item_img">     <img src="./assets/images/offer1.png" data-attr="alt" data-i18n_attr="image_alt" alt="" class="w-100">   </figure>   <div class="offer-content_item-text">     <p class="para" data-i18n="offer_item_text"></p>     <span class="price bolder" data-i18n="offer_item_price"></span>   </div> </div>

JavaScript’s role is to:

  1. Set an attribute called data-offer on each card.
  2. Assign a “special offer” or “best offer” value to it.

Finally, we can use the data-offer attribute in our style:

.offers__item::after {   content: attr(data-offer);   /* etc. */ }

Select by the dir attribute

Many languages are left-to-right, but some are not. We can specify what should be different in the [dir='rtl']. This attribute must be on the element itself or we can use nesting to reach the wanted element. Since we’ve already added the dir attribute to our HTML tag, then we can use it in nesting. We will use it later on our sample page.

4. Prepare the web fonts

In a multilingual website, we may also want to change the font family between languages because perhaps a particular font is more legible for a particular language.

Fallback fonts

We can benefit from the fallback by writing the right-to-left font after the default one.

font-family: 'Roboto', 'Tajawal', sans-serif;

This helps in cases where the default font doesn’t support right-to-left. That snippet above is using the Roboto font, which doesn’t support Arabic letters. If the default font supports right-to-left (like the Cairo font), and the design needs it to be changed, then this is not a perfect solution.

font-family: 'Cairo', 'Tajawal', sans-serif; /* won't work as expected */

Let’s look at another way.

Using CSS variables and the :lang() pseudo-class

We can mix the previous two technique where we change the font-family property value using custom properties that are re-assigned by the :lang pseudo class.

html {   --font-family: 'Roboto', sans-serif; }  html:lang(ar){   --font-family: 'Tajawal', sans-serif; }  html:lang(jp){   --font-family: 'Noto Sans JP', sans-serif; }

5. CSS Logical Properties

In CSS days past, we used to use left and right to define offsets along the x-axis, and the top and bottom properties to to define offsets along the y-axis. That makes direction switching a headache. Fortunately, CSS supports logical properties that define direction‐relative equivalents of the older physical properties. They support things like positioning, alignment, margin, padding, border, etc.

If the writing mode is horizontal (like English), then the logical inline direction is along the x-axis and the block direction refers to the y-axis. Those directions are flipped in a vertical writing mode, where inline travels the y-axis and and block flows along the x-axis.

Writing Mode x-axis y-axis
horizontal inline block
vertical  block inline

In other words, the block dimension is the direction perpendicular to the writing mode and the inline dimension is the direction parallel to the writing mode. Both inline and block levels have start and end values to define a specific direction. For example, we can use margin-inline-start instead of margin-left. This mean the margin direction automatically inverts when the page direction is rtl. It’s like our CSS is direction-aware and adapts when changing contexts.

There is another article on CSS-Tricks, Building Multi-Directional Layouts from Ahmad El-Alfy, that goes into the usefulness of building websites in multiple languages using logical properties.

This is exactly how we can handle margins, padding and borders. We’ll use them in the footer section to change which border gets the rounded edge.

The top-tight edge of the border is rounded in a default ltr writing mode.

As long as we’re using the logical equivalent of border-top-right-radius, CSS will handle that change for us.

.footer {   border-start-end-radius: 120px; }

Now, after switching to the rtl direction, it’ll work fine.

The “call to action” section is another great place to apply this:

.cta__text {   border-start-start-radius: 50%;   border-end-start-radius: 50px; } .cta__img {   border: 1px dashed var(--secondary-color);   border-inline-start-color: var(--light-color); }

Now, in Arabic, we get the correct layout:

You might be wondering exactly how the block and inline dimensions reverse when the writing mode changes. Back to the Japanese version, the text is from vertical, going from top-to-bottom. I added this line to the code:

/* The "About" section when langauge is Japanese */ html:lang(jp) .about__text {   margin-block-end: auto;   width: max-content; }

Although I added margin to the “block” level, it is applied it to the left margin. That’s because the text rotated 90 degrees when the language switched and flows in a vertical direction.

6. Other layout considerations

Even after all this prep, sometimes where elements move to when the direction and language change is way off. There are multiple factors at play, so let’s take a look.

Position

Using an absolute or fixed position to move elements may affect how elements shift when changing directions. Some designs need it. But I’d still recommend asking yourself: do I really need this?

Fro example, the newsletter subscription form in the footer section of our example can be implemented using position. The form itself takes the relative position, while the button takes the absolute position.

<form id="newsletter-form" class="relative">   <input type="email" data-attr="placeholder" data-i18n_attr="footer_input_placeholder" class="w-100">   <button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button> </form>
html[dir="ltr"] .footer__newsletter_btn {   right: 0; } html[dir="rtl"] .footer__newsletter_btn {   left: 0; }
This works fine in a rtl writing mode.

In the “hero” section, I made the background using a ::before pseudo-class with an absolute position:

<header class="hero relative">   <!-- etc. --> </header>
.hero {   background-image: linear-gradient(90deg, #30333f, #161616, #161616); } .hero::before  {   content: '';   display: block;   height: 100%;   width: 33.33%;   background-color: var(--primary-color);   clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);   position: absolute;   top: 0;   right: 0; }

Here’s the HTML for our hero element:

<header class="hero relative">   <!-- etc. -->   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Note that an .absolute class is in there that applies position: absolute to the hero section’s social widget. Meanwhile, the hero itself is relatively positioned.

How we move the social widget halfway down the y-axis:

.hero__social {   left: 0;   top: 50%;   transform: translateY(-50%); }

In the Arabic, we can fix the ::before pseudo-class position that is used in the background using the same technique we use in the footer form. That said, there are multiple issues we need to fix here:

  1. The clip-path direction
  2. The background linear-gradient
  3. The coffee-cup image direction
  4. The social media box’s position

Let’s use a simple flip trick instead. First, we wrap the hero content, and social content in two distinct wrapper elements instead of one:

<header class="hero relative">   <div class="hero__content">       <!-- etc. -->   </div>   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Then we rotate both of the hero wrappers—the social box inner wrapper and the image—180 degrees:

html[dir="rtl"] .hero, html[dir="rtl"] .hero__content, html[dir="rtl"] .hero__img img, html[dir="rtl"] .hero__social > div {   transform: rotateY(180deg); }

Yeah, that’s all. This simple trick is also helpful if the hero’s background is an image.

transform: translate()

This CSS property and value function helps move the element on one or more axes. The difference between ltr and rtl is that the x-axis is the inverse/negative value of the current value. So, we can store the value in a variable and change it according to the language.

html {   --about-img-background-move: -20%; }  html[dir='rtl']{   --about-img-background-move: 20%; }

We can do the same thing for the background image in the another section:

<figure class="about__img relative">   <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100"> </figure>
.about__img::after {   content: '';   position: absolute;   z-index: -1;   transform: translateY(-75%) translateX(var(--about-img-background-move));   /* etc. */ }

Margins

Margins are used to extend or reduce spaces between elements. It accepts negative values, too. For example, a positive margin-top value (20%) pushes the content down, while a negative value (e.g. -20%) pulls the content up.

If margins values are negative, then the top and left margins move the item up or to the left. However, the right and bottom margins do not. Instead, they pull content that is located in the right of the item towards the left, and the content underneath the item up. For example, if we apply a negative top margin and negative bottom margin together on the same item, the item is moved up and pull the content below it up into the item.

Here’s an example:

<section>   <div id="d1"></div>   <div id="d2"></div>   <div id="d3"></div> </section>
div {   width: 100px;   height: 100px;   border: 2px solid; } #d1 {   background-color: yellow;   border-color: red; } #d2 {   background-color: lightblue;   border-color: blue; } #d3 {   background-color: green;   border-color: red; }

The result of the above code should be something like this:

Let’s add these negative margins to the #d2 element:

#d2 {   margin-top: -40px;   margin-bottom: -70px; }

Notice how the second box in the diagram moves up, thanks to a negative margin-top value, and the green box also moves up an overlaps the second box, thanks to a negative margin-bottom value.

The next thing you might be asking: But what is the difference between transform: translate and the margins?

When moving an element with a negative margin, the initial space that is taken by the element is no longer there. But in the case of translating the element using a transform, the opposite is true. In other words, a negative margin leads pulls the element up, but the transform merely changes its position, without losing the space reserved for it.

Let’s stick to using margin in one direction:

#d2 {   margin-top: -40px;   /* margin-bottom: -70px; */ }

Now let’s replace the margin with the transform:

#d2 {   /* margin-top: -40px;*/   transform: translateY(-40px); }

You can see that, although the element is pulled up, its initial space is still there according to the natural document flow.

Flexbox

The display: flex provides a quick way to control the how the elements are aligned in their container. We can use align-items and justify-content to align child elements at the parent level.

In our example, we can use flexbox in almost every section to make the column layout. Plus, we can use it in the “offers” section to center the set of those yellow “special” and “best” marks:

.offers__item::after {   content: attr(data-offer);   display: flex;   align-items: center;   justify-content: center;   text-align: center; }

The same can be applied to the hero section to center text vertically.

<div class="d-lg-flex align-items-center">   <div class="hero__text d-xl-flex align-items-center">     <div>       <!-- text -->     </div>   </div>   <figure class="hero__img relative">     <img src="/image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </div>

If the flex-direction value is row, then we can benefit from controlling the width for each element. In the “hero” section, we need to set the image on the angled slope of the background where the color transitions from dark gray to yellow.

.hero__text {   width: 56.5%; } .hero__img {   width: 33.33%; }

Both elements take up a a total of 89.83% of the parent container’s width. Since we didn’t specify justify-content on the parent, it defaults to start, leaving the remaining width at the end.

We can combine the flexbox with any of the previous techniques we’ve seen, like transforms and margins. This can help us to reduce how many position instances are in our code. Let’s use it with a negative margin in the “call to action” section to locate the image.

<section class="cta d-xl-flex align-items-center">   <div class="cta__text w-100">     <!-- etc. -->   </div>   <figure class="cta__img">     <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </section>

Because we didn’t specify the flex-wrap and flex-basis properties, the image and the text both fit in the parent. However, since we used a negative margin, the image is pulled to the left, along with its width. This saves extra space for the text. We also want to use a logical property, inline-start, instead of left to handle switching to the rtl direction.

Grid

Finally, we can use a grid container to positing the elements. CSS Grid is powerful (and different than flexbox) in that it lays things along both the x-axis and the y-axis as opposed to only one of them.

Suppose that in the “offers” section, the role of the “see all” button is to get extra data that to display on the page. Here’s JavaScript code to repeat the current content:

// offers section ==> "see all" btn functionality (function(){   document.querySelector('.offers .btn').addEventListener('click', function(){     const offersContent = document.querySelector('.offers__content');     offersContent.innerHTML += offersContent.innerHTML;     offersContent.classList.remove('offers__content--has-margin');     this.remove();   }) })();

Then, let’s use display: grid in the CSS for this section. First, here’s our HTML, with our grid container highlighted.

<div class="offers__content offers__content--has-margin d-grid">   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div> </div>

We implement CSS Grid on the .offers__content element:

html {   /* custom properties */   --offers-content-column: repeat(3, 1fr);   --offers-content-gap: 5vw; }  .offers__content {   display: grid;   grid-template-columns: var(--offers-content-column);   gap: var(--offers-content-gap); } .offers__content--has-margin {   margin-block-end: 60px; }

This is the result after clicking the button:

Our page is far from being the best example of how CSS Grid works. While I was browsing the designs online, I found a design that uses the following structure:

Notice how CSS Grid makes the responsive layout without media queries. And as you might expect, it works well for changing writing modes where we adjust where elements go on the grid based on the current writing mode.

Wrapping up

Here is the final version of the page. I ensured to implement the responsiveness with a mobile-first approach to show you the power of the CSS variables. Be sure to open the demo in full page mode as well.

I hope these techniques help make creating multilingual designs easier for you. We looked at a bunch of CSS properties we can use to apply styles to specific languages. And we looked at different approaches to do that, like selecting the :lang pseudo-class and data attributes using the attr() function. As part of this, we covered what logical properties are in CSS and how they adapt to a document’s writing mode—which is so much nicer than having to write additional CSS rulesets to swap out physical property units that otherwise are unaffected by the writing mode.

We also checked out a number of different positioning and layout techniques, looking specifically at how different techniques are more responsive and maintainable than others. For example, CSS Grid and Flexbox are equipped with features that can re-align elements inside of a container based on changing conditions.

Clearly, there are lots of moving pieces when working with a multilingual site. There are probably other requirements you need to consider when optimizing a site for specific languages, but the stuff we covered here together should give you all of the layout-bending superpowers you need to create robust layouts that accommodate any number of languages and writing modes.


The post Control Layout in a Multi-Directional Website appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Control Layout in a Multi-Directional Website

Many business websites need a multilingual setup. As with anything development-related, implementing one in an easy, efficient, and maintainable way is desirable. Designing and developing to be ready for multiple languages, whether it happens right at launch or is expected to happen at any point in the future, is smart.

Changing the language and content is the easy part. But when you do that, sometimes the language you are changing to has a different direction. For example, text (and thus layout) in English flows left-to-right while text (and thus layout) in Arabic goes right-to-left.

In this article, I want to build a multilingual landing page and share some CSS techniques that make this process easier. Hopefully the next time you’ll need to do the same thing, you’ll have some implementation techniques to draw from.

We’ll cover six major points. I believe that the first five are straightforward. The sixth includes multiple options that you need to think about first.

1. Start with the HTML markup

The lang and dir attributes will define the page’s language and direction.

<!-- English, left-to-right --> <html lang="en" dir="ltr">  <!-- Arabic, right-to-left --> <html lang="ar" dir="rtl">

Then we can use these attributes in selectors to do the the styling. lang and dir attributes are on the HTML tag or a specific element in which the language varies from the rest of the page. Those attributes help improve the website’s SEO by showing the website in the right language for users who search for it in case that each language has a separate HTML document.

Also, we need to ensure that the charset meta tag is included and its value is UTF-8 since it’s the only valid encoding for HTML documents which also supports all languages.

<meta charset="utf-8">

I’ve prepared a landing page in three different languages for demonstration purposes. It includes the HTML, CSS, and JavaScript we need.

2. CSS Custom Properties are your friend

Changing the direction may lead to inverting some properties. So, if you used the CSS property left in a left-to-right layout, you probably need right in the right-to-left layout, and so on. And changing the language may lead to changing font families, font sizes, etc.

These multiple changes may cause unclean and difficult to maintain code. Instead, we can assign the value to a custom property, then change the value when needed. This is also great for responsiveness and other things that might need a toggle, like dark mode. We can change the font-size, margin, padding, colors, etc., in the blink of an eye, where the values then cascade to wherever needed.

Here are some of the CSS custom properties that we are using in this example:

html {   /* colors */   --dark-color: #161616;   --light-color: #eee;   --primary-text-color: var(--dark-color);   --primary-bg-color: #fff;   --shadow-color: var(--light-color);   --hero-bg-gradient: linear-gradient(90deg, #30333f, #161616, #161616);    /* font sizes */   --logo-font-size: 2rem;   --lang-switcher-font-size: 1.02em;   --offers-item-after-font-size: 1.5rem;    /* margin and padding */   --btn-padding: 7px;   --sec-padding-block: 120px;    /* height and width */   --hero-height: 500px;   --cta-img-width: 45.75%; }

While styling our page, we may add/change some of these custom properties, and that is entirely natural. Although this article is about multi-directional websites, here’s a quick example that shows how we can re-assign custom property values by having one set of values on the <body>, then another set when the <body> contains a .dark class:

body {   background-color: var(--primary-bg-color);   color: var(--primary-text-color); } body.dark {   --primary-bg-color: #0f0f0f;   --primary-text-color: var(--light-color);    /* other changes */   --shadow-color: #13151a;   --hero-bg-gradient: linear-gradient(90deg, #191b20, #131313, #131313); }

That’s the general idea. We’re going to use custom properties in the same sort of way, though for changing language directions.

3) CSS pseudo-classes and selectors

CSS has a few features that help with writing directions. The following two pseudo-classes and attribute are good examples that we can put to use in this example.

The :lang() pseudo-class

We can use :lang() pseudo-class to target specific languages and apply CSS property values to them individually, or together. For example, in this example, we can change the font size when the :lang pseudo-class switches to either Arabic or Japanese:

html:lang(ar), html:lang(jp){   --offers-item-after-font-size: 1.2rem;  }

Once we do that, we also need to change the writing-mode property from its horizontal left-to-right default direction to vertical right-to-left direction account:

html:lang(jp) .about__text {   writing-mode: vertical-rl; }

The :attr() pseudo-class

The :attr() pseudo-class helps makes the “content” of the pseudo-elements like ::before or ::after “dynamic” in a sense, where we can drop the dir HTML attribute into the CSS content property using the attr() function. That way, the value of dir determines what we’re selecting and styling.

<div dir="ltr"></div> <div dir="rtl"></div>
div::after {   content: attr(dir); }

The power is the ability to use any custom data attribute. Here, we’re using a custom data-name attribute whose value is used in the CSS:

<div data-name="English content" dir="ltr"></div> <div data-name="محتوى عربي" dir="rtl"></div>
div::after {   content: attr(data-name); }

This makes it relatively easy to change the content after switching that language without changing the style. But, back to our design. The three-up grid of cards has a yellow “special” or “best” off mark beside an image.

This is the HTML for each card:

<div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">   <figure class="offers__item_img">     <img src="./assets/images/offer1.png" data-attr="alt" data-i18n_attr="image_alt" alt="" class="w-100">   </figure>   <div class="offer-content_item-text">     <p class="para" data-i18n="offer_item_text"></p>     <span class="price bolder" data-i18n="offer_item_price"></span>   </div> </div>

JavaScript’s role is to:

  1. Set an attribute called data-offer on each card.
  2. Assign a “special offer” or “best offer” value to it.

Finally, we can use the data-offer attribute in our style:

.offers__item::after {   content: attr(data-offer);   /* etc. */ }

Select by the dir attribute

Many languages are left-to-right, but some are not. We can specify what should be different in the [dir='rtl']. This attribute must be on the element itself or we can use nesting to reach the wanted element. Since we’ve already added the dir attribute to our HTML tag, then we can use it in nesting. We will use it later on our sample page.

4. Prepare the web fonts

In a multilingual website, we may also want to change the font family between languages because perhaps a particular font is more legible for a particular language.

Fallback fonts

We can benefit from the fallback by writing the right-to-left font after the default one.

font-family: 'Roboto', 'Tajawal', sans-serif;

This helps in cases where the default font doesn’t support right-to-left. That snippet above is using the Roboto font, which doesn’t support Arabic letters. If the default font supports right-to-left (like the Cairo font), and the design needs it to be changed, then this is not a perfect solution.

font-family: 'Cairo', 'Tajawal', sans-serif; /* won't work as expected */

Let’s look at another way.

Using CSS variables and the :lang() pseudo-class

We can mix the previous two technique where we change the font-family property value using custom properties that are re-assigned by the :lang pseudo class.

html {   --font-family: 'Roboto', sans-serif; }  html:lang(ar){   --font-family: 'Tajawal', sans-serif; }  html:lang(jp){   --font-family: 'Noto Sans JP', sans-serif; }

5. CSS Logical Properties

In CSS days past, we used to use left and right to define offsets along the x-axis, and the top and bottom properties to to define offsets along the y-axis. That makes direction switching a headache. Fortunately, CSS supports logical properties that define direction‐relative equivalents of the older physical properties. They support things like positioning, alignment, margin, padding, border, etc.

If the writing mode is horizontal (like English), then the logical inline direction is along the x-axis and the block direction refers to the y-axis. Those directions are flipped in a vertical writing mode, where inline travels the y-axis and and block flows along the x-axis.

Writing Mode x-axis y-axis
horizontal inline block
vertical  block inline

In other words, the block dimension is the direction perpendicular to the writing mode and the inline dimension is the direction parallel to the writing mode. Both inline and block levels have start and end values to define a specific direction. For example, we can use margin-inline-start instead of margin-left. This mean the margin direction automatically inverts when the page direction is rtl. It’s like our CSS is direction-aware and adapts when changing contexts.

There is another article on CSS-Tricks, Building Multi-Directional Layouts from Ahmad El-Alfy, that goes into the usefulness of building websites in multiple languages using logical properties.

This is exactly how we can handle margins, padding and borders. We’ll use them in the footer section to change which border gets the rounded edge.

The top-tight edge of the border is rounded in a default ltr writing mode.

As long as we’re using the logical equivalent of border-top-right-radius, CSS will handle that change for us.

.footer {   border-start-end-radius: 120px; }

Now, after switching to the rtl direction, it’ll work fine.

The “call to action” section is another great place to apply this:

.cta__text {   border-start-start-radius: 50%;   border-end-start-radius: 50px; } .cta__img {   border: 1px dashed var(--secondary-color);   border-inline-start-color: var(--light-color); }

Now, in Arabic, we get the correct layout:

You might be wondering exactly how the block and inline dimensions reverse when the writing mode changes. Back to the Japanese version, the text is from vertical, going from top-to-bottom. I added this line to the code:

/* The "About" section when langauge is Japanese */ html:lang(jp) .about__text {   margin-block-end: auto;   width: max-content; }

Although I added margin to the “block” level, it is applied it to the left margin. That’s because the text rotated 90 degrees when the language switched and flows in a vertical direction.

6. Other layout considerations

Even after all this prep, sometimes where elements move to when the direction and language change is way off. There are multiple factors at play, so let’s take a look.

Position

Using an absolute or fixed position to move elements may affect how elements shift when changing directions. Some designs need it. But I’d still recommend asking yourself: do I really need this?

Fro example, the newsletter subscription form in the footer section of our example can be implemented using position. The form itself takes the relative position, while the button takes the absolute position.

<form id="newsletter-form" class="relative">   <input type="email" data-attr="placeholder" data-i18n_attr="footer_input_placeholder" class="w-100">   <button class="btn btn--tertiary footer__newsletter_btn bolder absolute" data-i18n="footer_newsLetter_btn"></button> </form>
html[dir="ltr"] .footer__newsletter_btn {   right: 0; } html[dir="rtl"] .footer__newsletter_btn {   left: 0; }
This works fine in a rtl writing mode.

In the “hero” section, I made the background using a ::before pseudo-class with an absolute position:

<header class="hero relative">   <!-- etc. --> </header>
.hero {   background-image: linear-gradient(90deg, #30333f, #161616, #161616); } .hero::before  {   content: '';   display: block;   height: 100%;   width: 33.33%;   background-color: var(--primary-color);   clip-path: polygon(20% 0%, 100% 0, 100% 100%, 0% 100%);   position: absolute;   top: 0;   right: 0; }

Here’s the HTML for our hero element:

<header class="hero relative">   <!-- etc. -->   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Note that an .absolute class is in there that applies position: absolute to the hero section’s social widget. Meanwhile, the hero itself is relatively positioned.

How we move the social widget halfway down the y-axis:

.hero__social {   left: 0;   top: 50%;   transform: translateY(-50%); }

In the Arabic, we can fix the ::before pseudo-class position that is used in the background using the same technique we use in the footer form. That said, there are multiple issues we need to fix here:

  1. The clip-path direction
  2. The background linear-gradient
  3. The coffee-cup image direction
  4. The social media box’s position

Let’s use a simple flip trick instead. First, we wrap the hero content, and social content in two distinct wrapper elements instead of one:

<header class="hero relative">   <div class="hero__content">       <!-- etc. -->   </div>   <div class="hero__social absolute">     <div class="d-flex flex-col">       <!-- etc. -->     </div>   </div> </header>

Then we rotate both of the hero wrappers—the social box inner wrapper and the image—180 degrees:

html[dir="rtl"] .hero, html[dir="rtl"] .hero__content, html[dir="rtl"] .hero__img img, html[dir="rtl"] .hero__social > div {   transform: rotateY(180deg); }

Yeah, that’s all. This simple trick is also helpful if the hero’s background is an image.

transform: translate()

This CSS property and value function helps move the element on one or more axes. The difference between ltr and rtl is that the x-axis is the inverse/negative value of the current value. So, we can store the value in a variable and change it according to the language.

html {   --about-img-background-move: -20%; }  html[dir='rtl']{   --about-img-background-move: 20%; }

We can do the same thing for the background image in the another section:

<figure class="about__img relative">   <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100"> </figure>
.about__img::after {   content: '';   position: absolute;   z-index: -1;   transform: translateY(-75%) translateX(var(--about-img-background-move));   /* etc. */ }

Margins

Margins are used to extend or reduce spaces between elements. It accepts negative values, too. For example, a positive margin-top value (20%) pushes the content down, while a negative value (e.g. -20%) pulls the content up.

If margins values are negative, then the top and left margins move the item up or to the left. However, the right and bottom margins do not. Instead, they pull content that is located in the right of the item towards the left, and the content underneath the item up. For example, if we apply a negative top margin and negative bottom margin together on the same item, the item is moved up and pull the content below it up into the item.

Here’s an example:

<section>   <div id="d1"></div>   <div id="d2"></div>   <div id="d3"></div> </section>
div {   width: 100px;   height: 100px;   border: 2px solid; } #d1 {   background-color: yellow;   border-color: red; } #d2 {   background-color: lightblue;   border-color: blue; } #d3 {   background-color: green;   border-color: red; }

The result of the above code should be something like this:

Let’s add these negative margins to the #d2 element:

#d2 {   margin-top: -40px;   margin-bottom: -70px; }

Notice how the second box in the diagram moves up, thanks to a negative margin-top value, and the green box also moves up an overlaps the second box, thanks to a negative margin-bottom value.

The next thing you might be asking: But what is the difference between transform: translate and the margins?

When moving an element with a negative margin, the initial space that is taken by the element is no longer there. But in the case of translating the element using a transform, the opposite is true. In other words, a negative margin leads pulls the element up, but the transform merely changes its position, without losing the space reserved for it.

Let’s stick to using margin in one direction:

#d2 {   margin-top: -40px;   /* margin-bottom: -70px; */ }

Now let’s replace the margin with the transform:

#d2 {   /* margin-top: -40px;*/   transform: translateY(-40px); }

You can see that, although the element is pulled up, its initial space is still there according to the natural document flow.

Flexbox

The display: flex provides a quick way to control the how the elements are aligned in their container. We can use align-items and justify-content to align child elements at the parent level.

In our example, we can use flexbox in almost every section to make the column layout. Plus, we can use it in the “offers” section to center the set of those yellow “special” and “best” marks:

.offers__item::after {   content: attr(data-offer);   display: flex;   align-items: center;   justify-content: center;   text-align: center; }

The same can be applied to the hero section to center text vertically.

<div class="d-lg-flex align-items-center">   <div class="hero__text d-xl-flex align-items-center">     <div>       <!-- text -->     </div>   </div>   <figure class="hero__img relative">     <img src="/image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </div>

If the flex-direction value is row, then we can benefit from controlling the width for each element. In the “hero” section, we need to set the image on the angled slope of the background where the color transitions from dark gray to yellow.

.hero__text {   width: 56.5%; } .hero__img {   width: 33.33%; }

Both elements take up a a total of 89.83% of the parent container’s width. Since we didn’t specify justify-content on the parent, it defaults to start, leaving the remaining width at the end.

We can combine the flexbox with any of the previous techniques we’ve seen, like transforms and margins. This can help us to reduce how many position instances are in our code. Let’s use it with a negative margin in the “call to action” section to locate the image.

<section class="cta d-xl-flex align-items-center">   <div class="cta__text w-100">     <!-- etc. -->   </div>   <figure class="cta__img">     <img src="image.jpg" data-attr="alt" data-i18n_attr="image_alt" class="w-100">   </figure> </section>

Because we didn’t specify the flex-wrap and flex-basis properties, the image and the text both fit in the parent. However, since we used a negative margin, the image is pulled to the left, along with its width. This saves extra space for the text. We also want to use a logical property, inline-start, instead of left to handle switching to the rtl direction.

Grid

Finally, we can use a grid container to positing the elements. CSS Grid is powerful (and different than flexbox) in that it lays things along both the x-axis and the y-axis as opposed to only one of them.

Suppose that in the “offers” section, the role of the “see all” button is to get extra data that to display on the page. Here’s JavaScript code to repeat the current content:

// offers section ==> "see all" btn functionality (function(){   document.querySelector('.offers .btn').addEventListener('click', function(){     const offersContent = document.querySelector('.offers__content');     offersContent.innerHTML += offersContent.innerHTML;     offersContent.classList.remove('offers__content--has-margin');     this.remove();   }) })();

Then, let’s use display: grid in the CSS for this section. First, here’s our HTML, with our grid container highlighted.

<div class="offers__content offers__content--has-margin d-grid">   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="special_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div>   <div class="offers__item relative" data-attr="data-offer" data-i18n_attr="best_offer">     <!-- etc. -->   </div> </div>

We implement CSS Grid on the .offers__content element:

html {   /* custom properties */   --offers-content-column: repeat(3, 1fr);   --offers-content-gap: 5vw; }  .offers__content {   display: grid;   grid-template-columns: var(--offers-content-column);   gap: var(--offers-content-gap); } .offers__content--has-margin {   margin-block-end: 60px; }

This is the result after clicking the button:

Our page is far from being the best example of how CSS Grid works. While I was browsing the designs online, I found a design that uses the following structure:

Notice how CSS Grid makes the responsive layout without media queries. And as you might expect, it works well for changing writing modes where we adjust where elements go on the grid based on the current writing mode.

Wrapping up

Here is the final version of the page. I ensured to implement the responsiveness with a mobile-first approach to show you the power of the CSS variables. Be sure to open the demo in full page mode as well.

I hope these techniques help make creating multilingual designs easier for you. We looked at a bunch of CSS properties we can use to apply styles to specific languages. And we looked at different approaches to do that, like selecting the :lang pseudo-class and data attributes using the attr() function. As part of this, we covered what logical properties are in CSS and how they adapt to a document’s writing mode—which is so much nicer than having to write additional CSS rulesets to swap out physical property units that otherwise are unaffected by the writing mode.

We also checked out a number of different positioning and layout techniques, looking specifically at how different techniques are more responsive and maintainable than others. For example, CSS Grid and Flexbox are equipped with features that can re-align elements inside of a container based on changing conditions.

Clearly, there are lots of moving pieces when working with a multilingual site. There are probably other requirements you need to consider when optimizing a site for specific languages, but the stuff we covered here together should give you all of the layout-bending superpowers you need to create robust layouts that accommodate any number of languages and writing modes.


The post Control Layout in a Multi-Directional Website appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Wrangling Control Over PDFs with the Adobe PDF Embed API

By our last estimate, there are now more PDFs in the world than atoms in the universe (not verified by outside sources) so chances are, from time to time, you’re going to run into a PDF document or two. Browsers do a reasonably good job of handling PDFs. Typically, clicking a link to a PDF will open a new tab in your browser with custom UI and rendering per browser. Here’s the same PDF opened in Edge, Chrome, Firefox, and Safari, respectively:

As expected, each browser puts its own spin on things but one thing is consistent — all of them take over the entire viewport to render the PDF. While this is useful for giving the reader as much real estate to consume the PDF as possible, it would sometimes be desirable to have more control over the PDF experience. This is where the Adobe PDF Embed API comes in. The PDF Embed API is a free JavaScript library that lets you display PDFs inline with the rest of your content along with giving you control over the tools UI, supporting annotations and events, and more. Let’s walk through some examples of what it’s like to work with the library.

Getting a key

Before we begin, you’ll need to register for a key. If you head over to our Getting Started page, you’ll see a link to let you create new credentials:

If you don’t have an account with Adobe yet you’ll need to create one. You’ll be prompted to give the credentials a name and an application domain. While the name isn’t terribly important, the application domain is. The key you get will be restricted to a particular domain. You can only enter one domain here, so to start, you can use localhost or use cdpn.io as the domain if you want to try it on CodePen. If you want to use the API in both local and production environments, you can create multiple projects in the console or use HOSTS file configurations. (The ability to specify multiple domains for credentials is on the radar.)

Hit the lovely blue “Create Credentials” button and you’ll get your key:

If you’re curious and want to see what the Embed API can do right away, click on “Get Code Samples” which brings you to an interactive online demo. But since we’re hardcore coders who build our own editors before we go to work, let’s dive right into a simple example.

Building a demo

First, let’s build an HTML page that hosts our PDF. I’ve been a web developer for twenty years and am now an expert at designing beautiful HTML pages. Here’s what I came up:

<html>   <head></head>   <body>     <h1>Cats are Everything</h1>     <p>       Cats are so incredibly awesome that I feel like       we should talk about them more. Here's a PDF       that talks about how awesome cats are.     </p> 		     <!-- PDF here! -->      <p>       Did you like that? Was it awesome? I think it was awesome!      </p>   </body> </html>

I put it up a bit of CSS, of course:

A heading one that says Cats are Everything, followed by two short paragraphs about cats. The text is white against a green background.

I honestly don’t know why Adobe hired me as a developer evangelist because, clearly, I should be on a design team. Anyway, how do we get our PDF in there? The first step is to add our library SDK:

<script src="https://documentcloud.adobe.com/view-sdk/main.js"></script>

Now we need a bit of JavaScript. When our library loads, it fires an event called adobe_dc_view_sdk.ready. Depending on how you load your scripts and your framework of choice, it’s possible the event fires before you even get a chance to check for it.

We can also check for the existence of window.AdobeDC. We can handle both by chaining to a function that will set up our PDF.

if (window.AdobeDC) displayPDF(); else {   document.addEventListener("adobe_dc_view_sdk.ready", () => displayPDF()); }  function displayPDF() {   console.log('Lets do some AWESOME PDF stuff!'); }

Alright, so how do we display the PDF? To accept all the defaults we can use the following snippet:

let adobeDCView = new AdobeDC.View({clientId: ADOBE_KEY, divId: "mypdf" }); adobeDCView.previewFile({   content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},   metaData:{fileName: "cat.pdf"} });

Let’s break that down. First, we create a new AdobeDC.View object. The clientId value is the key from earlier. The divId is the ID of a <div> in the DOM where the PDF will render. I removed the HTML comment I had earlier and dropped in an empty <div> with that ID. I also used some CSS to specify a width and height for it:

#mypdf {   width: 100%;   height: 500px; }

The previewFile method takes two main arguments. The first is the PDF URL. The PDF Embed API works with either URLs or File Promises. For URLs, we want to ensure we’ve got CORS setup properly. The second value is metadata about the PDF which, in this case, is the filename. Here’s the result:

Here’s a complete CodePen of the example, and yes, you can clone this, modify it, and continue to use the key.

You’ll notice the UI contains the same tools you would expect in any PDF viewer, along with things like the ability to add notes and annotations.

Note the “Save” icon in the figure above. When downloaded, the PDF will include the comments and lovely marker drawings.

Customizing the experience

Alright, you’ve seen the basic example, so let’s kick it up a bit and customize the experience. One of the first ways we may do that is by changing the embed mode which controls how the PDF is displayed. The library has four different ones supported:

  • Sized Container — The default mode used to render a PDF inside a <div> container. It renders one page at a time.
  • Full Window — Like Sized Container in that it will “fill” its parent <div>, but displays the entire PDF in one “stream” you can scroll through.
  • In-Line — Displays it in a web page, like Sized Container, but renders every page in a vertical stack. Obviously, don’t use this with some large 99-page PDF unless you hate your users. (But if you already display one of those “Sign up for our newsletter” modal windows when a person visits your site, or your site autoplays videos, then by all means, go ahead and do this.)
  • Lightbox — Displays the PDF in a centered window while greying out the rest of the content. The UI to close the display is automatically included.

To specify a different view, a second argument of options can be passed. For example:

function displayPDF() {   console.log('Lets do some AWESOME PDF stuff!');   let adobeDCView = new AdobeDC.View({clientId: ADOBE_KEY, divId: "mypdf" });   adobeDCView.previewFile({     content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},     metaData:{fileName: "cat.pdf"}   },    {     embedMode: "IN_LINE"   });	 }

Note that in in-line mode, the height specified for your div will be ignored so that the PDF can stretch it’s legs a bit. You can view this version of the demo here: https://codepen.io/cfjedimaster/pen/OJpJRKr

Let’s consider another example – using lightbox along with a button lets us give the user the chance to load the PDF when they want. We can modify our HTML like so:

<html>   <head></head>   <body>     <h1>Cats are Everything</h1>     <p>       Cats are so incredibly awesome that I feel like       we should talk about them more. Here's a PDF       that talks about how awesome cats are.     </p> 		     <!-- PDF here! -->     <button id="showPDF" disabled>Show PDF</button>      <p>       Did you like that? Was it awesome? I think it was awesome!      </p>   </body> </html>

I’ve added a disabled button to the HTML and removed the empty <div>. We won’t need it as the lightbox mode will use a modal view. Now we modify the JavaScript:

const ADOBE_KEY = 'b9151e8d6a0b4d798e0f8d7950efea91';  if(window.AdobeDC) enablePDF(); else {   document.addEventListener("adobe_dc_view_sdk.ready", () => enablePDF()); }  function enablePDF() {   let btn = document.querySelector('#showPDF');   btn.addEventListener('click', () => displayPDF());   btn.disabled = false; }  function displayPDF() {   console.log('Lets do some AWESOME PDF stuff!');   let adobeDCView = new AdobeDC.View({clientId: ADOBE_KEY });   adobeDCView.previewFile({     content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},     metaData:{fileName: "cat.pdf"}   },    {     embedMode: "LIGHT_BOX"   });	 }

There are two main changes here. First, checking that the library is loading (or has loaded) runs enablePDF, which removes the disabled property from the button and adds a click event. This runs displayPDF. Notice how the initializer does not use the divId anymore. Second, note the embedMode mode change. You can try this yourself via the Pen below.

You have more customization options as well, including tweaking the UI menus and icons to enable and disable various features:

adobeDCView.previewFile({ 	content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}}, 	metaData:{fileName: "cat.pdf"} },  { 	showDownloadPDF: false, 	showPrintPDF: false, 	showAnnotationTools: false, 	showLeftHandPanel: false });	

You can most likely guess what this does, but here’s a shot with the default options:

And here’s how it looks with those options disabled:

By the way, just so we’re clear, we definitely know that disabling the download button doesn’t “protect” the PDF seen here, the URL is still visible in via View Source.

Again, this is only a small example, so be sure to check the customization docs for more examples.

Working with the API and handling events

Along with customizing the UI, we also get fine grained control over the experience after it’s loaded. This is supported with an API that can return information about the PDF as well as the ability to listen for events.

Working with the API uses the result of the previewFile method. We haven’t used that yet, but it returns a Promise. One use of the API is to get metadata. Here’s an example:

let resultPromise = adobeDCView.previewFile({   content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},   metaData:{fileName: "cat.pdf"} }, { embedMode:"SIZED_CONTAINER" });	  resultPromise.then(adobeViewer => {   adobeViewer.getAPIs().then(apis => {     apis.getPDFMetadata()     .then(result => console.log(result))     .catch(error => console.log(error));   }); });

This returns:

{   'numPages':6,   'pdfTitle':'Microsoft Word - Document1',   'fileName':'' }

Along with API calls, we also have deep analytics integration. While the docs go into great detail (and talk about integration with Adobe Analytics), you can handle PDF viewing and interacting events in any way that makes sense to you.

For example, since we know how many pages are in a PDF, and we can listen for events like viewing a page, we can notice when a person has viewed every page. To build this, I modified the JavaScript, like so:

const ADOBE_KEY = 'b9151e8d6a0b4d798e0f8d7950efea91';  //used to track what we've read const pagesRead = new Set([1]); let totalPages, adobeDCView, shownAlert=false;  if(window.AdobeDC) displayPDF(); else {   document.addEventListener("adobe_dc_view_sdk.ready", () => displayPDF()); }  function displayPDF() {   console.log('Lets do some AWESOME PDF stuff!');   adobeDCView = new AdobeDC.View({clientId: ADOBE_KEY, divId: "mypdf" }); 	   let resultPromise = adobeDCView.previewFile({     content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},     metaData:{fileName: "cat.pdf"}   }, { embedMode:"SIZED_CONTAINER" });	    resultPromise.then(adobeViewer => {     adobeViewer.getAPIs().then(apis => {       apis.getPDFMetadata()       .then(result => {         totalPages = result.numPages;         console.log('totalPages', totalPages);         listenForReads();       })       .catch(error => console.log(error));     });   }); 	 }  function listenForReads() { 	   const eventOptions = {     enablePDFAnalytics: true   }    adobeDCView.registerCallback(   AdobeDC.View.Enum.CallbackType.EVENT_LISTENER,   function(event) {     let page = event.data.pageNumber;     pagesRead.add(page);     console.log(`view page $ {page}`);     if(pagesRead.size === totalPages && !shownAlert) {       alert('You read it all!');       shownAlert = true;     }   }, eventOptions );  }

Notice that after I get information about the page count, I run a function that starts listening for page viewing events. I use a Set to record each unique page, and when the total equals the number of pages in the PDF, I alert a message. (Of course, we don’t know if the reader actually read the text.) While admiditely a bit lame, you can play with this yourself here:

const ADOBE_KEY = 'b9151e8d6a0b4d798e0f8d7950efea91';  //used to track what we've read const pagesRead = new Set([1]); let totalPages, adobeDCView, shownAlert=false;  if(window.AdobeDC) displayPDF(); else {   document.addEventListener("adobe_dc_view_sdk.ready", () => displayPDF()); }  function displayPDF() {   console.log('Lets do some AWESOME PDF stuff!');   adobeDCView = new AdobeDC.View({clientId: ADOBE_KEY, divId: "mypdf" }); 	   let resultPromise = adobeDCView.previewFile({     content:{location: {url: "https://static.raymondcamden.com/enclosures/cat.pdf"}},     metaData:{fileName: "cat.pdf"}   }, { embedMode:"SIZED_CONTAINER" });	    resultPromise.then(adobeViewer => {     adobeViewer.getAPIs().then(apis => {       apis.getPDFMetadata()       .then(result => {         totalPages = result.numPages;         console.log('totalPages', totalPages);         listenForReads();       })       .catch(error => console.log(error));     });   }); 	 }  function listenForReads() { 	   const eventOptions = {     listenOn: [ AdobeDC.View.Enum.PDFAnalyticsEvents.PAGE_VIEW ],     enablePDFAnalytics: true   }    adobeDCView.registerCallback(     AdobeDC.View.Enum.CallbackType.EVENT_LISTENER,     function(event) {       /*        console.log("Type " + event.type);        console.log("Data " + JSON.stringify(event.data));       */       let page = event.data.pageNumber;       pagesRead.add(page);       console.log(`view page $ {page}`);       if(pagesRead.size === totalPages && !shownAlert) {         alert('You read it all!');         shownAlert = true;       }     }, eventOptions   );  }

How to learn more

I hope this introduction to the Embed API has been useful. Here are some resources to help you get deeper into it:

  • Start off by perusing the docs as it does a great job going over all the details.
  • We’ve got a live demo that lets you see everything in action and will even generate code for you.
  • If you have questions or need support, we’ve got a forum for questions and you can use the adobe-embed-api on StackOverflow as well.
  • If you need to work with PDFs at the server level, we’ve got the Adobe PDF Tools API as well as a crazy cool Adobe Document Generation tool you may like. These aren’t free like the PDF Embed API, but you can trial them for six months and test them out by signing up.

Lastly, we are absolutely open to feedback on this. If you’ve got suggestions, ideas, questions, or anything else, feel free to reach out!


The post Wrangling Control Over PDFs with the Adobe PDF Embed API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , ,
[Top]

React Authentication & Access Control

Authentication and access control are required for most applications, but they often distract us from building core features. In this article, I’ll cover a straightforward way to add auth and access control in React.

Instead of adding a static library that you have to keep up to date or re-research each time you build a project, we’ll use a service that stays up to date automatically and is a much simpler alternative to Auth0, Okta, and others.

React authentication

There are two main things your React application needs to do to sign on a user:

  1. Get an access token from an authentication server
  2. Send the access token to your backend server with each subsequent request

These steps are the same for pretty much all authentication, whether that’s standard email and password, magic links, or single sign on (SSO) providers like Google, Azure, or Facebook.

Ultimately, we want our React app to send an initial request to an authentication server and have that server generate an access token we can use.

JWT access tokens

There are different choices for what type of access token to use, and JSON Web Tokens (JWTs) are a great option. JWTs are compact, URL-safe tokens that your React application can use for authentication and access control.

Each JWT has a JSON object as its “payload” and is signed such that your backend server can verify that the payload is authentic. An example JWT looks like:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9.f7iKN-xi24qrQ5NQtOe0jiriotT-rve3ru6sskbQXnA 

The payload for this JWT is the middle section (separated by periods):

eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9

The JWT payload can be decoded from base64 to yield the JSON object:

JSON.parse(atob("eyJ1c2VySWQiOjEsImF1dGhvcml6YXRpb24iOiJhZG1pbiJ9"));  // => {   “userId”: 1,   “authorization”: “admin” }

It’s important to note that this payload is readable by anyone with the JWT, including your React application or a third party. Anyone that has the JWT can read its contents.

However, only the authentication server can generate valid JWTs. Your React application, your backend server, or a malicious third party cannot generate valid JWTs, only read and verify them.

When your backend server receives a request with a JWT, it should verify the JWT as authentic by checking it against the public key for that JWT. This allows your application server to verify incoming JWTs and reject any tokens that were not created by the authentication server (or that have expired).

The flow for using a JWT in your React application looks like this:

  1. Your React app requests a JWT from the authentication server whenever the user wants to sign on.
  2. The authentication server generates a JWT using a private key and then sends the JWT back to your React app.
  3. Your React app stores this JWT and sends it to your backend server whenever your user needs to make a request.
  4. Your backend server verifies the JWT using a public key and then reads the payload to determine which user is making the request.

Each of these steps is simple to write down, but each step has its own pitfalls when you actually want to implement it and keep it secure. Especially over time, as new threat vectors emerge and new platforms need to be patched or supported, the security overhead can add up quickly.

Userfront removes auth complexity in React apps

Userfront is a framework that abstracts away auth complexity. This makes it much easier for you to work with authentication in a React application and, perhaps most importantly, it keeps all the auth protocols updated for you automatically over time.

The underlying philosophy with Userfront is that world-class auth should not take effort – it should be easy to set up, and security updates should happen for you automatically. Userfront has all the bells and whistles of authentication, Single Sign On (SSO), access control, and multi-tenancy, with a production-ready free tier up to 10,000 monthly active users.

For most modern React applications, it’s a great solution.

Setting up authentication in React

Now we’ll go through building all the main aspects of authentication in a React application. The final code for this example is available here.

Set up your React application and get your build pipeline in order however you prefer. In this tutorial, we’ll use Create React App, which does a lot of the setup work for us, and we’ll also add React Router for client-side routing. Start by installing Create React App and React Router:

npx create-react-app my-app cd my-app npm install react-router-dom --save npm start

Now our React application is available at http://localhost:3000.

Like the page says, we can now edit the src/App.js file to start working.

Replace the contents of src/App.js with the following, based on the React Router quickstart:

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return <h2>Home</h2>; }  function Login() {   return <h2>Login</h2>; }  function PasswordReset() {   return <h2>Password Reset</h2>; }  function Dashboard() {   return <h2>Dashboard</h2>; }

Now we have ourselves a very simple app with routing:

Route Description
/ Home page
/login Login page
/reset Password reset page
/dashboard User dashboard, for logged in users only

This is all the structure we need to start adding authentication.

Signup, login, and password reset with Userfront

Go ahead and create a Userfront account. This will give you a signup form, login form, and password reset form you can use for the next steps.

In the Toolkit section of your Userfront dashboard, you can find the instructions for installing your signup form:

Follow the instructions by installing the Userfront React package with:

npm install @userfront/react --save npm start

Add the signup form to your home page by importing and initializing Userfront, and then updating the Home() function to render the signup form.

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import Userfront from "@userfront/react";  Userfront.init("demo1234");  const SignupForm = Userfront.build({   toolId: "nkmbbm", });  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return (     <div>       <h2>Home</h2>       <SignupForm />     </div>   ); }  function Login() {   return <h2>Login</h2>; }  function PasswordReset() {   return <h2>Password Reset</h2>; }  function Dashboard() {   return <h2>Dashboard</h2>; }

Now the home page has your signup form. Try signing up a user:

Your signup form is in “Test mode” by default, which will create user records in a test environment you can view separately in your Userfront dashboard:

Continue by adding your login and password reset forms in the same way that you added your signup form:

// src/App.js  import React from "react"; import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom"; import Userfront from "@userfront/react";  Userfront.init("demo1234");  const SignupForm = Userfront.build({   toolId: "nkmbbm", }); const LoginForm = Userfront.build({   toolId: "alnkkd", }); const PasswordResetForm = Userfront.build({   toolId: "dkbmmo", });  export default function App() {   return (     <Router>       <div>         <nav>           <ul>             <li>               <Link to="/">Home</Link>             </li>             <li>               <Link to="/login">Login</Link>             </li>             <li>               <Link to="/reset">Reset</Link>             </li>             <li>               <Link to="/dashboard">Dashboard</Link>             </li>           </ul>         </nav>          <Switch>           <Route path="/login">             <Login />           </Route>           <Route path="/reset">             <PasswordReset />           </Route>           <Route path="/dashboard">             <Dashboard />           </Route>           <Route path="/">             <Home />           </Route>         </Switch>       </div>     </Router>   ); }  function Home() {   return (     <div>       <h2>Home</h2>       <SignupForm />     </div>   ); }  function Login() {   return (     <div>       <h2>Login</h2>       <LoginForm />     </div>   ); }  function PasswordReset() {   return (     <div>       <h2>Password Reset</h2>       <PasswordResetForm />     </div>   ); }  function Dashboard() {   return <h2>Dashboard</h2>; }

At this point, your signup, login, and password reset should all be functional.

Your users can sign up, log in, and reset their password.

Access control in React

Usually, we don’t want users to be able to view the dashboard unless they are logged in. This is known as a protected route.

Whenever a user is not logged in but tries to visit /dashboard, we can redirect them to the login screen.

We can accomplish this by updating the Dashboard component in src/App.js to handle the conditional logic.

When a user is logged in with Userfront, they will have an access token available as Userfront.accessToken(). We can check for this token to determine if the user is logged in. If the user is logged in, we can show the dashboard page, and if the user is not logged in, we can redirect to the login page.

Add the Redirect component to the import statement for React Router, and then update the Dashboard component to redirect if no access token is present.

// src/App.js  import React from "react"; import {   BrowserRouter as Router,   Switch,   Route,   Link,   Redirect, // Be sure to add this import } from "react-router-dom";  // ...  function Dashboard() {   function renderFn({ location }) {     // If the user is not logged in, redirect to login     if (!Userfront.accessToken()) {       return (         <Redirect           to={{             pathname: "/login",             state: { from: location },           }}         />       );     }      // If the user is logged in, show the dashboard     const userData = JSON.stringify(Userfront.user, null, 2);     return (       <div>         <h2>Dashboard</h2>         <pre>{userData}</pre>         <button onClick={Userfront.logout}>Logout</button>       </div>     );   }    return <Route render={renderFn} />; }

Notice also that we’ve added a logout button by calling Userfront.logout() directly:

<button onClick={Userfront.logout}>Logout</button>

Now, when a user is logged in, they can view the dashboard. If the user is not logged in, they will be redirected to the login page.

React authentication with an API

You’ll probably want to retrieve user-specific information from your backend. In order to protect your API endpoints, your backend server should check that incoming JWTs are valid.

There are many libraries available to read and verify JWTs across various languages; here are a few popular libraries for handling JWTs:

Node.js .NET Python Java

Your access token

While the user is logged in, their access token is available in your React application as Userfront.accessToken().

Your React application can send this as a Bearer token inside the Authorization header to your backend server. For example:

// Example of calling an endpoint with a JWT  import Userfront from "@userfront/react"; Userfront.init("demo1234");  async function getInfo() {   const res = await window.fetch("/your-endpoint", {     method: "GET",     headers: {       "Content-Type": "application/json",       Authorization: `Bearer $ {Userfront.accessToken()}`,     },   });    console.log(res); }  getInfo();

To handle a request like this, your backend should read the JWT from the Authorization header and verify that it is valid using the public key found in your Userfront dashboard.

Here is an example of Node.js middleware to read and verify the JWT access token:

// Node.js example (Express.js)  const jwt = require("jsonwebtoken");  function authenticateToken(req, res, next) {   // Read the JWT access token from the request header   const authHeader = req.headers["authorization"];   const token = authHeader && authHeader.split(" ")[1];   if (token == null) return res.sendStatus(401); // Return 401 if no token    // Verify the token using the Userfront public key   jwt.verify(token, process.env.USERFRONT_PUBLIC_KEY, (err, auth) => {     if (err) return res.sendStatus(403); // Return 403 if there is an error verifying     req.auth = auth;     next();   }); }

Using this approach, any invalid or missing tokens would be rejected by your server. You can also reference the contents of the token later in the route handlers using the req.auth object:

console.log(req.auth);  // => {   mode: 'test',   tenantId: 'demo1234',   userId: 5,   userUuid: 'ab53dbdc-bb1a-4d4d-9edf-683a6ca3f609',   isConfirmed: false,   authorization: {     demo1234: {       tenantId: 'demo1234',       name: 'Demo project',       roles: ["admin"],       permissions: []     },   },   sessionId: '35d0bf4a-912c-4429-9886-cd65a4844a4f',   iat: 1614114057,   exp: 1616706057 }

With this information, you can perform further checks as desired, or use the userId or userUuid to look up user information to return.

For example, if you wanted to limit a route to admin users only, you could check against the authorization object from the verified access token, and reject any tokens that don’t have an admin role:

// Node.js example (Express.js)  app.get("/users", (req, res) => {   const authorization = req.auth.authorization["demo1234"] || {};    if (authorization.roles.includes("admin")) {     // Allow access   } else {     // Deny access   } });

React SSO (Single Sign On)

With your Toolkit forms in place, you can add social identity providers like Google, Facebook, and LinkedIn to your React application, or business identity providers like Azure AD, Office365, and more.

You do this by creating an application with the identity provider (e.g. Google), and then adding that application’s credentials to the Userfront dashboard. The result is a modified sign on experience:

No additional code is needed to implement Single Sign On using this approach: you can add and remove providers without updating your forms or the way you handle JWTs.

Final notes

React authentication and access control can be complex to do yourself, or it can be simple when using a service.

Both the setup step and, more importantly, the maintenance over time, are handled with modern platforms like Userfront.

JSON Web Tokens allow you to cleanly separate your auth token generation layer from the rest of your application, making it easier to reason about and more modular for future needs. This architecture also allows you to focus your efforts on your core application, where you are likely to create much more value for yourself or your clients.

For more details on adding auth to your React application, visit the Userfront guide, which covers everything from setting up your auth forms to API documentation, example repositories, working with different languages and frameworks, and more.


The post React Authentication & Access Control appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , ,
[Top]

Weekly Platform News: Focus Rings, Donut Scope, Ditching em Units, and Global Privacy Control

In this week’s news, Chrome tackles focus rings, we learn how to get “donut” scope, Global Privacy Control gets big-name adoption, it’s time to ditch pixels in media queries, and a snippet that prevents annoying form validation styling.

Chrome will stop displaying focus rings when clicking buttons

Chrome, Edge, and other Chromium-based browsers display a focus indicator (a.k.a. focus ring) when the user clicks or taps a (styled) button. For comparison, Safari and Firefox don’t display a focus indicator when a button is clicked or tapped, but do only when the button is focused via the keyboard.

The focus ring will stay on the button until the user clicks somewhere else on the page.

Some developers find this behavior annoying and are using various workarounds to prevent the focus ring from appearing when a button is clicked or tapped. For example, the popular what-input library continuously tracks the user’s input method (mouse, keyboard or touch), allowing the page to suppress focus rings specifically for mouse clicks.

[data-whatintent="mouse"] :focus {   outline: none; }

A more recent workaround was enabled by the addition of the CSS :focus-visible pseudo-class to Chromium a few months ago. In the current version of Chrome, clicking or tapping a button invokes the button’s :focus state but not its :focus-visible state. that way, the page can use a suitable selector to suppress focus rings for clicks and taps without affecting keyboard users.

:focus:not(:focus-visible) {   outline: none; }

Fortunately, these workarounds will soon become unnecessary. Chromium’s user agent stylesheet recently switched from :focus to :focus-visible, and as a result of this change, button clicks and taps no longer invoke focus rings. The new behavior will first ship in Chrome 90 next month.

The enhanced CSS :not() selector enables “donut scope”

I recently wrote about the A:not(B *) selector pattern that allows authors to select all A elements that are not descendants of a B element. This pattern can be expanded to A B:not(C *) to create a “donut scope.”

For example, the selector article p:not(blockquote *) matches all <p> elements that are descendants of an <article> element but not descendants of a <blockquote> element. In other words, it selects all paragraphs in an article except the ones that are in a block quotation.

The donut shape that gives this scope its name

The New York Times now honors Global Privacy Control

Announced last October, Global Privacy Control (GPC) is a new privacy signal for the web that is designed to be legally enforceable. Essentially, it’s an HTTP Sec-GPC: 1 request header that tells websites that the user does not want their personal data to be shared or sold.

The DuckDuckGo Privacy Essentials extension enables GPC by default in the browser

The New York Times has become the first major publisher to honor GPC. A number of other publishers, including The Washington Post and Automattic (WordPress.com), have committed to honoring it “this coming quarter.”

From NYT’s privacy page:

Does The Times support the Global Privacy Control (GPC)?

Yes. When we detect a GPC signal from a reader’s browser where GDPR, CCPA or a similar privacy law applies, we stop sharing the reader’s personal data online with other companies (except with our service providers).

The case for em-based media queries

Some browsers allow the user to increase the default font size in the browser’s settings. Unfortunately, this user preference has no effect on websites that set their font sizes in pixels (e.g., font-size: 20px). In part for this reason, some websites (including CSS-Tricks) instead use font-relative units, such as em and rem, which do respond to the user’s font size preference.

Ideally, a website that uses font-relative units for font-size should also use em values in media queries (e.g., min-width: 80em instead of min-width: 1280px). Otherwise, the site’s responsive layout may not always work as expected.

For example, CSS-Tricks switches from a two-column to a one-column layout on narrow viewports to prevent the article’s lines from becoming too short. However, if the user increases the default font size in the browser to 24px, the text on the page will become larger (as it should) but the page layout will not change, resulting in extremely short lines at certain viewport widths.

If you’d like to try out em-based media queries on your website, there is a PostCSS plugin that automatically converts min-width, max-width, min-height, and max-height media queries from px to em.

(via Nick Gard)

A new push to bring CSS :user-invalid to browsers

In 2017, Peter-Paul Koch published a series of three articles about native form validation on the web. Part 1 points out the problems with the widely supported CSS :invalid pseudo-class:

  • The validity of <input> elements is re-evaluated on every key stroke, so a form field can become :invalid while the user is still typing the value.
  • If a form field is required (<input required>), it will become :invalid immediately on page load.

Both of these behaviors are potentially confusing (and annoying), so websites cannot rely solely on the :invalid selector to indicate that a value entered by the user is not valid. However, there is the option to combine :invalid with :not(:focus) and even :not(:placeholder-shown) to ensure that the page’s “invalid” styles do not apply to the <input> until the user has finished entering the value and moved focus to another element.

The CSS Selectors module defines a :user-invalid pseudo-class that avoids the problems of :invalid by only matching an <input> “after the user has significantly interacted with it.”

Firefox already supports this functionality via the :-moz-ui-invalid pseudo-class (see it in action). Mozilla now intends to un-prefix this pseudo-class and ship it under the standard :user-invalid name. There are still no signals from other browser vendors, but the Chromium and WebKit bugs for this feature have been filed.


The post Weekly Platform News: Focus Rings, Donut Scope, Ditching em Units, and Global Privacy Control appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , , , , , , , , ,
[Top]

Give Users Control: The Media Session API

Here’s a scenario. You start a banging Kendrick Lamar track in one of your many open browser tabs. You’re loving it, but someone walks into your space and you need to pause it. Which tab is it? Browsers try to help with that a little bit. You can probably mute the entire system audio. But wouldn’t it be nice to actually have control over the audio playback without necessarily needing to find your way back to that tab?

The Media Session API makes this possible. It gives media playback access to the user outside of the browser tab where it is playing. If implemented, it will be available in various places on the device, including:

  • the notifications area on many mobile devices,
  • on other wearables, and
  • the media hub area of many desktop devices.

In addition, the Media Session API allows us to control media playback with media keys and voice assistants like Siri, Google Assistant, Bixby, or Alexa.

The Media Session API

The Media Session API mainly consists of the two following interfaces:

  • MediaMetadata
  • MediaSession

The MediaMetadata interface is what provides data about the playing media. It is responsible for letting us know the media’s title, album, artwork and artist (which is Kendrick Lamar in this example). The MediaSession interface is what is responsible for the media playback functionality.

Before we take a deep dive into the topic, we would have to take note of feature detection. It is good practice to check if a browser supports a feature before implementing it. To check if a browser supports the Media Session API, we would have to include the following in our JavaScript file:

if ('mediaSession' in navigator) {   // Our media session api that lets us seek to the beginning of Kendrick Lamar's &quot;Alright&quot; }

The MediaMetadata interface

The constructor, MediaMetadata.MediaMetadata() creates a new MediaMetadata object. After creating it, we can add the following properties:

  • MediaMetadata.title sets or gets the title of the media playing.
  • MediaMetadata.artist sets or gets the name of the artist or group of the media playing.
  • MediaMetadata.album sets or gets the name of the album containing the media playing.
  • MediaMetadata.artwork sets or gets the array of images related with the media playing.

The value of the artwork property of the MediaMetadata object is an array of MediaImage objects. A MediaImage object contains details describing an image associated with the media. The objects have the three following properties:

  • src: the URL of the image
  • sizes: indicates the size of the image so one image does not have to be scaled
  • type: the MIME type of the image

Let’s create a MediaMetadata object for Kendrick Lamar’s “Alright” off his To Pimp a Butterfly album.

if ('mediaSession' in navigator) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   }); }

The MediaSession interface

As stated earlier, this is what lets the user control the playback of the media. We can perform the following actions on the playing media through this interface:

  • play: play the media
  • pause: pause the media
  • previoustrack: switch to the previous track
  • nexttrack: switch to the next track
  • seekbackward: seek backward from the current position, by a few seconds
  • seekforward: seek forward from the current position, by a few seconds
  • seekto: seek to a specified time from the current position
  • stop: stop media playback
  • skipad: skip past the advertisement playing, if any

The MediaSessionAction enumerated type makes these actions available as string types. To support any of these actions, we have to use the MediaSession’s setActionHandler() method to define a handler for that action. The method takes the action, and a callback that is called when the user invokes the action. Let us take a not-too-deep dive to understand it better.

To set handlers for the play and pause actions, we include the following in our JavaScript file:

let alright = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('play', () => {     alright.play();   });   navigator.mediaSession.setActionHandler('pause', () => {     alright.pause();   }); }

Here we set the track to play when the user plays it and pause when the user pauses it through the media interface.

For the previoustrack and nexttrack actions, we include the following:

let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('previoustrack', () => {     u.play();   });   navigator.mediaSession.setActionHandler('nexttrack', () => {     forSaleInterlude.play();   }); }

This might not completely be self-explanatory if you are not much of a Kendrick Lamar fan but hopefully, you get the gist. When the user wants to play the previous track, we set the previous track to play. When it is the next track, it is the next track.

To implement the seekbackward and seekforward actions, we include the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);   });   navigator.mediaSession.setActionHandler('seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);   }); }

Given that I don’t consider any of this self-explanatory, I would like to give a concise explanation about the seekbackward and seekforward actions. The handlers for both actions, seekbackward and seekforward, are fired, as their names imply, when the user wants to seek backward or forward by a few number of seconds. The MediaSessionActionDetails dictionary provides us the “few number of seconds” in a property, seekOffset. However, the seekOffset property is not always present because not all user agents act the same way. When it is not present, we should set the track to seek backward or forward by a “few number of seconds” that makes sense to us. Hence, we use 10 seconds because it is quite a few. In a nutshell, we set the track to seek by seekOffset seconds if it is provided. If it is not provided, we seek by 10 seconds.

To add the seekto functionality to our Media Session API, we include the following snippet:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       return;     }     alright.currentTime = details.seekTime;   }); }

Here, the MediaSessionActionDetails dictionary provides the fastSeek and seekTime properties. fastSeek is basically seek performed rapidly (like fast-forwarding or rewinding) while seekTime is the time the track should seek to. While fastSeek is an optional property, the MediaSessionActionDetails dictionary always provides the seekTime property for the seekto action handler. So fundamentally, we set the track to fastSeek to the seekTime when the property is available and the user fast seeks, while we just set it to the seekTime when the user just seeks to a specified time.

Although I wouldn’t know why one would want to stop a Kendrick song, it won’t hurt to describe the stop action handler of the MediaSession interface:

if ('mediaSession' in navigator) {   navigator.mediaSession.setActionHandler('stop', () => {     alright.pause();     alright.currentTime = 0;   }); } 

The user invokes the skipad (as in, “skip ad” rather than “ski pad”) action handler when an advertisement is playing and they want to skip it so they can continue listening to Kendrick Lamar’s “Alright track. If I’m being honest, the complete details of the skipad action handler is out of the scope of my “Media Session API” understanding. Hence, you should probably look that up on your own after reading this article, if you actually want to implement it.

Wrapping up

We should take note of something. Whenever the user plays the track, seeks, or changes the playback rate, we are supposed to update the position state on the interface provided by the Media Session API. What we use to implement this is the setPositionState() method of the mediaSession object, as in the following:

if ('mediaSession' in navigator) {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }

In addition, I would like to remind you that not all browsers of the users would support all the actions. Therefore, it is recommended to set the action handlers in a try...catch block, as in the following:

const actionsAndHandlers = [   ['play', () => { /*...*/ }],   ['pause', () => { /*...*/ }],   ['previoustrack', () => { /*...*/ }],   ['nexttrack', () => { /*...*/ }],   ['seekbackward', (details) => { /*...*/ }],   ['seekforward', (details) => { /*...*/ }],   ['seekto', (details) => { /*...*/ }],   ['stop', () => { /*...*/ }] ]   for (const [action, handler] of actionsAndHandlers) {   try {     navigator.mediaSession.setActionHandler(action, handler);   } catch (error) {     console.log(`The media session action, $ {action}, is not supported`);   } }

Putting everything we have done, we would have the following:

let alright = new HTMLAudioElement(); let u = new HTMLAudioElement(); let forSaleInterlude = new HTMLAudioElement();  const updatePositionState = () => {   navigator.mediaSession.setPositionState({     duration: alright.duration,     playbackRate: alright.playbackRate,     position: alright.currentTime   }); }   const actionsAndHandlers = [   ['play', () => {     alright.play();     updatePositionState();   }],   ['pause', () => { alright.pause(); }],   ['previoustrack', () => { u.play(); }],   ['nexttrack', () => { forSaleInterlude.play(); }],   ['seekbackward', (details) => {     alright.currentTime = alright.currentTime - (details.seekOffset || 10);     updatePositionState();   }],   ['seekforward', (details) => {     alright.currentTime = alright.currentTime + (details.seekOffset || 10);     updatePositionState();   }],   ['seekto', (details) => {     if (details.fastSeek && 'fastSeek' in alright) {       alright.fastSeek(details.seekTime);       updatePositionState();       return;     }     alright.currentTime = details.seekTime;     updatePositionState();   }],   ['stop', () => {     alright.pause();     alright.currentTime = 0;   }], ]   if ( 'mediaSession' in navigator ) {   navigator.mediaSession.metadata = new MediaMetadata({     title: 'Alright',     artist: 'Kendrick Lamar',     album: 'To Pimp A Butterfly',     artwork: [       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/96x96', sizes: '96x96', type: 'image/png' },       { src: 'https://mytechnicalarticle/kendrick-lamar/to-pimp-a-butterfly/alright/128x128', sizes: '128x128', type: 'image/png' },       // More sizes, like 192x192, 256x256, 384x384, and 512x512     ]   });     for (const [action, handler] of actionsAndHandlers) {     try {       navigator.mediaSession.setActionHandler(action, handler);     } catch (error) {       console.log(`The media session action, $ {action}, is not supported`);     }   } }

Here’s a demo of the API:

I implemented six of the actions. Feel free to try the rest during your leisure.

If you view the Pen on your mobile device, notice how it appears on your notification area.

If your smart watch is paired to your device, take a sneak peek at it.

If you view the Pen on Chrome on desktop, navigate to the media hub and play with the media buttons there. The demo even has multiple tracks, so you experiment moving forward/back through tracks.

If you made it this far (or not), thanks for reading and please, on the next app you create with media functionality, implement this API.


The post Give Users Control: The Media Session API appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

More Control Over CSS Borders With background-image

You can make a typical CSS border dashed or dotted. For example:

.box {    border: 1px dashed black;    border: 3px dotted red; }

You don’t have all that much control over how big or long the dashes or gaps are. And you certainly can’t give the dashes slants, fading, or animation! You can do those things with some trickery though.

Amit Sheen build this really neat Dashed Border Generator:

The trick is using four multiple backgrounds. The background property takes comma-separated values, so by setting four backgrounds (one along the top, right, bottom, and left) and sizing them to look like a border, it unlocks all this control.

So like:

.box {   background-image: repeating-linear-gradient(0deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(90deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(180deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px), repeating-linear-gradient(270deg, #333333, #333333 10px, transparent 10px, transparent 20px, #333333 20px);   background-size: 3px 100%, 100% 3px, 3px 100% , 100% 3px;   background-position: 0 0, 0 0, 100% 0, 0 100%;   background-repeat: no-repeat; }

I like gumdrops.


The post More Control Over CSS Borders With background-image appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Get Programmatic Control of your Builds with Netlify Build Plugins

Today at Jamstack_Conf, Netlify announced Build Plugins. What it does is allow you to have particular hooks for events within your build, like when the build starts or ends. What’s nice about them is that they’re just a plain ‘ol JavaScript object, so you can insert some logic or kick off a library just the way you typically would within your application.

A “Build” is when you give your site to Netlify either via GitHub/GitLab/etc., or by literally just dropping the directory into the interface, Netlify will process all the assets, download and install packages, and generate a static version of the site to deploy to CDNs all around the world.

What the Build Plugin does is give you access to key points in time during that process, for instance, onPreBuild, onPostBuild, onSuccess, and so forth. You can execute some logic at those specific points in time, like this:

module.exports = {   onPreBuild: () => {     console.log('Hello world from onPreBuild event!')   }, }

You don’t only have to build them yourself, either! You can use build plugins that have been made by the community. There are very interesting ones, such as a11y, Cypress for testing, Inline Critical CSS, and my personal favorite, Subfont, which optimizes fonts for you in a really incredible way (you can watch a video about that).

Enable them through the dashboard through a few button clicks:

If you’d like to learn more, check out the announcement post here! Happy building!

The post Get Programmatic Control of your Builds with Netlify Build Plugins appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Building an accessible autocomplete control

Here’s a great in-depth post from Adam Silver about his journey to create an autocomplete field that’s as accessible as possible. There are so many edge cases to consider! There are old browsers and their peculiar quirks, there are accessibility best practices for screen readers, and not to mention dealing with the component design when there’s no JavaScript, etc.

Adam offers a warning before he begins:

[…] I’ve been looking at ways to let users enter a destination country. Unfortunately, native HTML form controls just aren’t good enough for this type of interaction. And so we need to build a custom autocomplete control from scratch. A word of warning though: this is one of the hardest UI components I’ve ever had to make—they’re just way harder than they look.

I also just bought Adam’s book, Form Design Patterns, and this post now makes me extra excited to read it.

Direct Link to ArticlePermalink

The post Building an accessible autocomplete control appeared first on CSS-Tricks.

CSS-Tricks

, , ,
[Top]