Designing for Different Devices (Viewport Size, Touch/Hover, etc.)

This document outlines the APIs used to adapt Discourse’s user interface for different devices.

Viewport Size

The most important characteristic to consider is the viewport size. We design “mobile first” and then add customizations for larger devices as needed. The breakpoints we use are:

Breakpoint Size Pixels (at 16px body font size)
sm 40rem 640px
md 48rem 768px
lg 64rem 1024px
xl 80rem 1280px
2xl 96rem 1536px

To use these in an SCSS file, add @use "lib/viewport"; at the top of the file, then use one of the available mixins:

@use "lib/viewport";

@include viewport.from(lg) {
  // SCSS rules here will be applied to
  // devices larger than the lg breakpoint
}

@include viewport.until(sm) {
  // SCSS rules here will be applied to
  // devices smaller than the sm breakpoint
}

@include viewport.between(sm, md) {
  // SCSS rules here will be applied to
  // devices with a size between the sm
  // and md breakpoints
}

In general, SCSS is the recommended way to handle layout differences based on viewport size. For advanced cases, the same breakpoints can be accessed in Ember components via the capabilities service. For example:

import Component from "@glimmer/component";
import { service } from "@ember/service";

class MyComponent extends Component {
  @service capabilities;

  <template>
    {{#if this.capabilities.viewport.lg}}
      This text will be displayed for devices larger than the lg breakpoint
    {{/if}}

    {{#unless this.capabilities.viewport.sm}}
      This text will be displayed for devices smaller than the sm breakpoint
    {{/unless}}
  </template>
}

These properties are reactive, and Ember will automatically re-render the relevant parts of the template as the browser is resized.

Touch & Hover

Some devices only have touchscreens, some only have a traditional mouse pointer, and some have both. Importantly, touchscreen users cannot “hover” over elements. Therefore, interfaces should be designed to work entirely without hover states, with hover-specific enhancements added for devices that support them.

There are several ways to detect touch/hover capability via CSS and JavaScript. For consistency, we recommend using Discourse’s helpers instead of those CSS/JS APIs directly.

For CSS, you can target the .discourse-touch and .discourse-no-touch classes, which are added to the <html> element. These are determined based on the (any-pointer: coarse) media query.

For example:

html.discourse-touch {
  // SCSS rules here will apply to devices with a touch screen,
  // including mobiles/tablets and laptops/desktops with touch screens.
}

html.discourse-no-touch {
  // SCSS rules here will apply to devices with no touch screen.
}

This information is also available in Ember components via the capabilities service:

import Component from "@glimmer/component";
import { service } from "@ember/service";

class MyComponent extends Component {
  @service capabilities;

  <template>
    {{#if this.capabilities.touch}}
      This text will be displayed for devices with a touch screen
    {{/if}}

    {{#unless this.capabilities.touch}}
      This text will be displayed for devices with no touch screen
    {{/unless}}
  </template>
}

Legacy Mobile / Desktop Modes

Historically, Discourse shipped two completely different layouts and stylesheets for “mobile” and “desktop” views, based on the browser’s user-agent. Developers would target these modes by putting CSS in specific mobile/desktop directories, by using the .mobile-view/.desktop-view HTML classes, and the site.mobileView boolean in JavaScript.

These techniques are now considered deprecated and should be replaced with the viewport and capability-based strategies discussed above. We will be removing the dedicated modes in the near future, making “mobile mode” an alias for “viewport width less than sm” for backwards compatibility.


This document is version controlled - suggest changes on github.

9 个赞

那么类似这样的东西将被弃用?

@service site;
...
const mobileView = this.site.mobileView;

如果在静态上下文中执行此操作,那么是的,这与即将推出的“基于视口的移动模式”(目前已禁用)不兼容。

如果像这样在自动跟踪上下文中执行检查:

@service site;
...

<template>
  {{#if this.site.mobileView}}
    ...
  {{/if}}
</template>

那么 Ember 将在 mobileView 布尔值更改时(即浏览器大小调整时)自动重新渲染内容。所以没关系。

所以可以确定的是,将其放入 getter 中已弃用,但将其放入 <template> 中则没有?

将其放入 getter 中也是可以的,因为 Ember 会自动跟踪它。

@service site;

get shouldRender() {
  return this.site.mobileView;
}

<template>
  {{#if this.shouldRender}}
    ...
  {{/if}}
</template>

^^ 这样就可以了

一个不好的例子是

export default apiInitializer((api) => {
  if (api.container.lookup("service:site").mobileView) {
    api.renderInOutlet("some-outlet", <template>My content</template>)
  }
});

因为在这种情况下,mobileView 只在应用程序启动时进行检查。调整浏览器大小不会重新运行初始化程序。

所以你会重构为类似这样的东西

export default apiInitializer((api) => {
  const site = api.container.lookup("service:site");
  api.renderInOutlet("some-outlet", <template>
    {{#if site.mobileView}}My content{{/if}}
  </template>);
});

这样,当调整浏览器大小时,对 mobileView 的更改就会生效。

3 个赞

明白了。谢谢你的解释!

1 个赞

(作者已删除帖子)

1 个赞

普遍的建议是:不要这样做。为什么这类体验会因屏幕尺寸而异?

一个有用的思想实验是:您期望它在折叠手机或平板电脑上如何表现,这些设备并不明确属于移动/桌面类别。

如果您确实希望这种行为更改基于浏览器的用户代理(就像旧的移动/桌面模式一样),那么我们有 capabilities.isMobileDevice,它会检查用户代理字符串中是否包含“mobile”一词:

1 个赞

嗯,在我的例子中,我为桌面端提供了将主页切换到标签交叉路由的选项——它的界面在移动设备上不存在……(尽管路由实际上有效——附加控件已隐藏)

……但你说得对,我可能会重新考虑!

1 个赞

有意思!我想知道这是故意的吗……我觉得它应该能在任何设备上运行 :thinking:

2 个赞