在多方向网站中控制布局

Avatar of Alaa Abd El-Rahim
Alaa Abd El-Rahim on

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 价值 200 美元的免费积分!

许多商业网站都需要多语言设置。与任何与开发相关的事物一样,以一种简单、高效和可维护的方式实现多语言设置是可取的。为多种语言做好设计和开发准备,无论是在发布时还是预计在未来任何时候发生,都是明智之举。

更改语言和内容是容易的部分。但是,当您这样做时,有时您要更改的语言具有不同的方向。例如,英语的文本(以及布局)从左到右流动,而阿拉伯语的文本(以及布局)从右到左流动。

在这篇文章中,我想构建一个多语言登录页面并分享一些使此过程更容易的 CSS 技术。希望下次您需要做同样的事情时,您将有一些实现技术可以借鉴。

我们将涵盖六个主要要点。我相信前五个很简单。第六个包括您需要首先考虑的多个选项。

1. 从 HTML 标记开始

langdir 属性将定义页面的语言和方向。

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

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

然后,我们可以在选择器中使用这些属性来进行样式设置。langdir 属性位于 HTML 标记语言与页面其余部分不同的特定元素上。这些属性通过在用户搜索网站时以正确的语言显示网站,从而帮助提高网站的 SEO(假设每种语言都有独立的 HTML 文档)。

此外,我们需要确保包含charset 元标记,并且其值为 UTF-8,因为它是唯一有效的 HTML 文档编码,它也支持所有语言。

<meta charset="utf-8">

为了演示目的,我准备了一个三种不同语言的登录页面。它包含我们需要的 HTML、CSS 和 JavaScript。

2. CSS 自定义属性是你的朋友

更改方向可能会导致某些属性反转。因此,如果您在从左到右的布局中使用了 CSS 属性 left,则可能需要在从右到左的布局中使用 right,依此类推。更改语言可能会导致更改字体系列、字体大小等。

这些多项更改可能会导致代码混乱且难以维护。相反,我们可以将值分配给自定义属性,然后在需要时更改值。这对于响应式设计和其他可能需要切换的事物(如暗模式)也很有用。我们可以改变font-sizemarginpadding、颜色等,在眨眼之间,这些值就会级联到任何需要的地方。

以下是我们在本示例中使用的一些 CSS 自定义属性

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%;
}

在为页面设置样式时,我们可能会添加/更改这些自定义属性中的某些属性,这是很自然的。虽然本文是关于多方向网站的,但这里有一个快速示例,展示了如何通过在<body> 上设置一组值,然后在<body> 包含.dark 类时设置另一组值来重新分配自定义属性值

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);
}

这就是一般思路。我们将以类似的方式使用自定义属性,但用于更改语言方向。

3) CSS 伪类和选择器

CSS 有一些特性可以帮助编写方向。以下两个伪类和属性是我们可以在这个例子中使用的很好的例子。

:lang() 伪类

我们可以使用 :lang() 伪类来定位特定语言并分别或一起向它们应用 CSS 属性值。例如,在本示例中,我们可以更改 :lang 伪类切换到阿拉伯语或日语时,字体大小也会改变

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

完成此操作后,我们还需要将writing-mode 属性从其水平从左到右的默认方向更改为垂直从右到左的方向帐户

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

:attr() 伪类

:attr() 伪类有助于使伪元素(如 ::before::after)的“内容”在某种程度上“动态”,我们可以使用 attr() 函数将 dir HTML 属性放到 CSS content 属性中。这样,dir 的值决定了我们选择和设置样式的内容。

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

它的强大之处在于能够使用任何自定义数据属性。这里,我们使用了一个自定义 data-name 属性,其值在 CSS 中使用

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

这样,在切换语言后,可以很容易地更改内容,而无需更改样式。但让我们回到我们的设计。卡片的三列网格在图像旁边有一个黄色的“特殊”或“最佳”标记。

这是每张卡片的 HTML

<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 的作用是

  1. 在每张卡片上设置一个名为 data-offer 的属性。
  2. 为其分配“特价优惠”或“最佳优惠”值。

最后,我们可以在样式中使用 data-offer 属性

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

dir 属性选择

许多语言是从左到右的,但有些不是。我们可以在 [dir='rtl'] 中指定应该有所不同的地方。此属性必须位于元素本身,或者我们可以使用嵌套来到达所需的元素。由于我们已经将 dir 属性添加到 HTML 标记中,因此我们可以在嵌套中使用它。我们将在我们的示例页面中稍后使用它。

4. 准备网页字体

在多语言网站中,我们可能还想在语言之间更改字体系列,因为某种字体可能更适合某种语言。

回退字体

通过在默认字体之后编写从右到左的字体,我们可以从回退中获益。

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

这在默认字体不支持从右到左的情况下很有帮助。上面那段代码使用的是Roboto 字体,它不支持阿拉伯字母。如果默认字体支持从右到左(如 Cairo 字体),而设计需要更改它,那么这不是一个完美的解决方案。

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

让我们看看另一种方法。

使用 CSS 变量和 :lang() 伪类

我们可以将前两种技术混合使用,使用 :lang 伪类重新分配的自定义属性来更改 font-family 属性值。

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 逻辑属性

在过去 CSS 的日子里,我们使用 `left` 和 `right` 来定义沿 x 轴的偏移量,以及 `top` 和 `bottom` 属性来定义沿 y 轴的偏移量。这使得方向切换变得很头疼。幸运的是,CSS 支持逻辑属性,它们定义了旧的物理属性的方向相关等效项。它们支持诸如定位、对齐、边距、填充、边框等。

如果书写模式是水平的(如英语),则逻辑 `inline` 方向沿 x 轴,`block` 方向是指向 y 轴。这些方向在垂直书写模式中被翻转,其中 `inline` 沿着 `y 轴` 方向,而 `block` 沿着 `x 轴` 方向流动。

书写模式x 轴y 轴
水平inlineblock
垂直 blockinline

换句话说,`block` 维度是垂直于书写模式的方向,而 `inline` 维度是平行于书写模式的方向。`inline` 和 `block` 级别都有 `start` 和 `end` 值来定义特定方向。例如,我们可以使用 `margin-inline-start` 代替 `margin-left`。这意味着当页面方向为 `rtl` 时,边距方向会自动反转。就像我们的 CSS 能够感知方向并在改变上下文时进行调整。

在 CSS-Tricks 上还有另一篇文章,来自 Ahmad El-Alfy 的 构建多方向布局,文章详细介绍了使用逻辑属性构建多语言网站的实用性。

这正是我们处理边距、填充和边框的方式。我们将在页脚部分使用它们来更改哪个边框获得圆角。

在默认的 `ltr` 书写模式中,边框的顶部紧边是圆角的。

只要我们使用 `border-top-right-radius` 的逻辑等效项,CSS 就会为我们处理这种更改。

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

现在,在切换到 rtl 方向后,它将正常工作。

“号召性用语”部分是另一个应用此方法的绝佳位置。

.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);
}

现在,在阿拉伯语中,我们得到了正确的布局。

您可能想知道当书写模式改变时,`block` 和 `inline` 维度是如何反转的。回到日语版本,文本是垂直的,从上到下流动。我在代码中添加了这一行。

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

虽然我在“block”级别添加了边距,但它被应用于左侧边距。这是因为文本在语言切换时旋转了 90 度,并以垂直方向流动。

6. 其他布局注意事项

即使经过了所有这些准备,有时元素在方向和语言改变时移动到哪里也是不确定的。有很多因素在起作用,所以让我们来看看。

位置

使用绝对或固定的 `position` 来移动元素可能会影响元素在改变方向时的移动方式。一些设计需要它。但我仍然建议问问自己:我真的需要它吗?

例如,我们示例中页脚部分的时事通讯订阅表单可以使用 `position` 实现。表单本身采用 `relative` 位置,而按钮采用 `absolute` 位置。

<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;
}
这在 `rtl` 书写模式中运行良好。

在“hero”部分,我使用带有 `absolute` 位置的 `::before` 伪类制作了背景。

<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;
}

这是我们 hero 元素的 HTML。

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

请注意,`absolute` 类在其中,它将 `position: absolute` 应用于 hero 部分的社交小部件。同时,hero 本身是相对定位的。

我们如何将社交小部件移动到 y 轴的中间位置。

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

在阿拉伯语中,我们可以使用与我们在页脚表单中使用的相同技术来修复用于背景的 `::before` 伪类位置。也就是说,这里有几个问题需要修复。

  1. `clip-path` 方向
  2. 背景 `linear-gradient`
  3. 咖啡杯图像方向
  4. 社交媒体框的位置

让我们使用一个简单的翻转技巧。首先,我们将 hero 内容和社交内容包装在两个不同的包装元素中,而不是一个。

<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>

然后,我们将两个 hero 包装器(社交框内部包装器和图像)旋转 180 度。

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

是的,就是这样。如果 hero 的背景是图像,这个简单的技巧也很有用。

`transform: translate()`

此 CSS 属性和值函数有助于在一条或多条轴上移动元素。`ltr` 和 `rtl` 之间的区别在于 x 轴是当前值的逆值/负值。因此,我们可以将值存储在一个变量中,并根据语言更改它。

html {
  --about-img-background-move: -20%;
}

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

我们可以在另一个部分对背景图像执行相同的操作。

<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. */
}

边距

边距用于扩展或缩小元素之间的空间。它也接受负值。例如,正 `margin-top` 值(`20%`)将内容向下推,而负值(例如 `-20%`)将内容向上拉。

如果边距值为负数,则顶部和左侧边距会将项目向上或向左移动。但是,右侧和底部边距不会这样做。相反,它们会将位于项目右侧的内容向左拉,并将项目下方的内容向上拉。例如,如果我们在同一项目上同时应用负顶部边距和负底部边距,则项目将向上移动并将下方内容向上拉入项目中。

这是一个示例。

<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;
}

上面代码的结果应该是这样的。

让我们将这些负边距添加到 `#d2` 元素中。

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

注意,由于负 `margin-top` 值,图中的第二个框向上移动,而由于负 `margin-bottom` 值,绿色框也向上移动并与第二个框重叠。

您可能想知道:`transform: translate` 和边距之间有什么区别?

当使用负边距移动元素时,元素最初占用的空间不再存在。但在使用 transform 平移元素的情况下,情况恰恰相反。换句话说,负边距会导致向上拉动元素,而 transform 仅仅改变其位置,而不会失去为其保留的空间。

让我们坚持在一个方向上使用边距。

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

现在让我们用 transform 替换边距。

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

您可以看到,虽然元素被向上拉,但其初始空间仍然存在,根据自然文档流。

Flexbox

`display: flex` 提供了一种快速方法来控制元素在其容器中的对齐方式。我们可以使用 `align-items` 和 `justify-content` 来在父级对齐子元素。

在我们的示例中,我们几乎可以在每个部分使用 flexbox 来制作列布局。此外,我们可以在“优惠”部分使用它来居中那一组黄色的“特价”和“最佳”标记。

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

同样可以应用于“hero”部分,以垂直居中文本。

<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>

如果 `flex-direction` 值为 `row`,那么我们可以从控制每个元素的宽度中获益。在“hero”部分,我们需要将图像放置在背景颜色的倾斜斜坡上,颜色从深灰色过渡到黄色。

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

这两个元素占据了父容器宽度总量的 89.83%。由于我们没有在父级上指定 `justify-content`,因此它默认为 `start`,将剩余的宽度留在末尾。

我们可以将 flexbox 与我们已经见过的任何技术相结合,例如 transform 和边距。这可以帮助我们减少代码中的 `position` 实例数量。让我们在“号召性用语”部分与负边距一起使用它来定位图像。

<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>

因为我们没有指定 `flex-wrap` 和 `flex-basis` 属性,所以图像和文本都适合父级。但是,由于我们使用了负边距,图像及其宽度一起被拉到左侧。这样可以为文本节省额外的空间。我们还想使用逻辑属性 `inline-start` 代替 `left` 来处理切换到 `rtl` 方向。

网格

最后,我们可以使用一个grid容器来定位元素。CSS Grid功能强大(并且不同于flexbox),因为它可以沿x轴和y轴排列元素,而不是仅沿其中一个轴排列。

假设在“优惠”部分,“查看全部”按钮的作用是获取要显示在页面上的额外数据。这是重复当前内容的 JavaScript 代码

// 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();
  })
})();

然后,让我们在 CSS 中为该部分使用display: grid。首先,这是我们的 HTML,突出显示了网格容器。

<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>

我们在.offers__content元素上实现 CSS Grid

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;
}

这是点击按钮后的结果

我们的页面远非 CSS Grid 工作方式的最佳示例。当我浏览在线设计时,我发现一个使用以下结构的设计

请注意 CSS Grid 如何在没有媒体查询的情况下实现响应式布局。正如您所预期的那样,它适用于更改写作模式,我们根据当前写作模式调整元素在网格上的位置。

总结

这是页面的最终版本。我确保以移动优先的方式实现响应能力,向您展示 CSS 变量的强大功能。请确保在全页面模式下打开演示。

我希望这些技术能帮助您更轻松地创建多语言设计。我们查看了可以用来将样式应用于特定语言的许多 CSS 属性。我们还研究了不同的方法来做到这一点,例如选择:lang伪类以及使用attr()函数的数据属性。作为其中的一部分,我们介绍了 CSS 中的逻辑属性以及它们如何适应文档的写作模式,这比不得不编写额外的 CSS 规则集来交换不受写作模式影响的物理属性单位要好得多。

我们还查看了许多不同的定位和布局技术,特别关注不同的技术如何比其他技术更具响应性和可维护性。例如,CSS Grid 和 Flexbox 配备了功能,可以在更改条件下重新排列容器内的元素。

显然,处理多语言网站时有很多活动部件。在针对特定语言优化网站时,您可能还需要考虑其他要求,但我们在这里一起介绍的内容应该为您提供创建强大的布局所需的所有布局弯曲超级大国,以适应任何数量的语言和写作模式。