跪拜 Guibai
← Back to the summary

Huawei's 16:9.5 Pura X View Forces a Rethink of Mobile CSS Adaptation

Huawei has launched another device — the new model unveiled on August 20 is named Pura X View.

This is also the world's first wide bar phone, feeling somewhat similar to an enlarged version of the outer screen of the previously released wide foldable?

image.png

I don't care about the price (because I probably can't afford it), but the screen size caught my attention — this device's screen aspect ratio is 16:9.5.

Not the traditional mobile device ratios of 3:2, 16:9, or even 21:9.

Faced with an endless stream of new devices and new aspect ratios, front-end development engineers, no!

Agent full-stack development engineers, how should we handle adaptation?

Solution

Let's first sort out the current mainstream mobile adaptation solutions:

rem solution (old classic, once the king)

The rem solution was widely used before. rem is relative to the root element's html font-size.

JS dynamically modifies the html font-size based on screen width. You write px in the page, and tools convert it to rem.

The early Taobao Mobile lib-flexible solution worked on this principle.

This solution has excellent compatibility, especially for older Android devices.

But because JS participates in the adaptation, it easily creates strong coupling between CSS and JS.

flex + percentage

Box widths and heights use percentages, and layout uses flex for elastic scaling.

This solution suits "fluid layouts," but it doesn't work for inner pages, especially for fixed-size buttons, icons, etc.

So it's generally only used for "homepages/first screens."

Media queries

Using @media to dynamically execute multiple sets of CSS based on screen width. This solution still exists on many older websites.

The implementation principle is simple and easy to understand, but the drawback is obvious: one set of styles per screen width range, which is too cumbersome to write.

It was used a lot for multi-device compatible official websites before, but was gradually abandoned later.

initial-scale dynamic scaling

Using JS to dynamically modify the meta viewport's initial-scale, scaling all screens to match the design draft.

For example, if the design draft is 750 and the device is 375, setting scale=0.5 achieves a perfect match.

The advantage is obvious: you only need JS to change the scaling.

The drawback is even more obvious: it looks terrible on special screen sizes.

Especially on high-resolution screens and small screens, some content may be scaled to the point of being completely illegible.

vw/vh solution (the ultimate solution)

This solution is currently the mobile adaptation method used by mainstream large domestic companies.

Using the postcss-px-to-viewport plugin to convert px in code to vw/vh viewport units.

Bypassing complex code adaptation, it operates at the compilation level.

The entire process is pure CSS implementation, with no JS intrusion.

Usage Method

Let's focus on how to use this solution, using Vue3 + Vite as an example.

First, modify index.html and set the meta.

<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">

Install the postcss-px-to-viewport plugin:

npm i postcss-px-to-viewport --save-dev

# Note: Vite cannot use the original version above; you need to switch to the plugin version
npm install postcss-px-to-viewport-8-plugin autoprefixer -D

Configure the plugin conversion by creating a postcss.config.js in the root directory:

export default {
  plugins: {
    autoprefixer: {},
    'postcss-px-to-viewport-8-plugin': {
      unitToConvert: 'px',  // Unit to convert
      viewportWidth: 750,  // UI design draft width, uniformly 750 for mobile
      unitPrecision: 5,  // vw decimal precision after conversion, 5 is sufficient
      propList: ['*'],  // Convert all properties
      viewportUnit: 'vw',
      fontViewportUnit: 'vw',
      selectorBlackList: ['.ignore-vw', '.hairline'],  // Elements with these class names are not converted
      minPixelValue: 1,  // Don't convert values less than 1px to avoid 0.xvw hairline anomalies
      mediaQuery: false,  // Keep px for media query breakpoints, don't convert to vw
      replace: true,
      exclude: [],
      landscape: false
    }
  }
}

You don't need to modify vite.config.js here; Vite automatically reads postcss.config.js.

Now you can use it in your code:

.card {
    width: 375px;
}

After compilation, this becomes 50vw.

However, you'll find a problem in the project: some styles generated by the plugin may have issues.

On small screens, they appear very small and are completely illegible.

On large screens, they appear excessively large, visible even to an 80-year-old.

So you need to add a constraint to the converter: the clamp() method.

Check the official documentation explanation:

image.png

Therefore, to prevent elements from being too large or too small, we change it to:

.card {
    width: clamp(120px, 375px, 500px);
}

Isn't it annoyingly tedious to write? So in engineering, we generally encapsulate it using less/scss mixins.

@mixin font-vw($base-px, $min-px, $max-px){
  font-size: clamp(#{$min-px}, calc(#{$base-px} / 750 * 100vw), #{$max-px});
}

// Usage
.text {
  @include font-vw(16px,14px,18px);
}

But a new problem arises: importing this mixin.less in every page is also quite troublesome, so we switch to a global injection approach.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  css: {
    preprocessorOptions: {
      less: {
        // Globally inject the mixin file; all vue component less environments automatically take effect without manual import
        additionalData: `@import "${path.resolve(__dirname, './src/styles/mixin.less')}";`,
        javascriptEnabled: true
      }
    }
  }
})

Now, mixin.less is imported by default wherever style lang="less" is used in each vue page.

So, the "ultimate form" emerges:

  1. Set the viewport meta
  2. Install postcss-px-to-viewport and set configuration items
  3. Install less and set up mixin.less injection
  4. Write clamp boundary style code

Utility Mixins

Here's a relatively complete set of mixin.less I wrote previously; just copy and use it in your project:

// ========== Global Adaptation Base Variables ==========
// Design draft base width
@design-width: 750px;

// Common font thresholds
@font-min: 12px;
@font-max: 20px;

// Common size thresholds
@size-min: 10px;
@size-max: 100%;

// Common border-radius thresholds
@radius-min: 4px;
@radius-max: 20px;

// Common spacing thresholds
@space-min: 4px;
@space-max: 30px;

// ========== Font Size Adaptation ==========
// Font adaptation: base value, min value, max value
.m-font(@base, @min: @font-min, @max: @font-max) {
  font-size: clamp(@min, @base, @max);
}

// Bold font adaptation
.m-font-bold(@base, @min: @font-min, @max: @font-max) {
  font-size: clamp(@min, @base, @max);
  font-weight: bold;
}

// Regular font size presets (project unified specification, call directly without parameters)
.m-font-sm() { .m-font(12px); }
.m-font-normal() { .m-font(14px); }
.m-font-base() { .m-font(16px); }
.m-font-lg() { .m-font(18px); }
.m-font-xl() { .m-font(20px); }
.m-font-xxl() { .m-font(24px, 20px, 28px); }

// ========== Width and Height Size Adaptation ==========
// Width adaptation
.m-width(@base, @min: @size-min, @max: @size-max) {
  width: clamp(@min, @base, @max);
}

// Height adaptation
.m-height(@base, @min: @size-min, @max: @size-max) {
  height: clamp(@min, @base, @max);
}

// Simultaneous width and height adaptation (square/fixed ratio modules)
.m-size(@base, @min: @size-min, @max: @size-max) {
  .m-width(@base, @min, @max);
  .m-height(@base, @min, @max);
}

// ========== Margin Adaptation (Inner and Outer Spacing) ==========
// Unified padding adaptation
.m-padding(@base, @min: @space-min, @max: @space-max) {
  padding: clamp(@min, @base, @max);
}

// Unified margin adaptation
.m-margin(@base, @min: @space-min, @max: @space-max) {
  margin: clamp(@min, @base, @max);
}

// Vertical padding
.m-padding-v(@base, @min: @space-min, @max: @space-max) {
  padding-top: clamp(@min, @base, @max);
  padding-bottom: clamp(@min, @base, @max);
}

// Horizontal padding
.m-padding-h(@base, @min: @space-min, @max: @space-max) {
  padding-left: clamp(@min, @base, @max);
  padding-right: clamp(@min, @base, @max);
}

// ========== Border Radius Adaptation ==========
// Border radius adaptation
.m-radius(@base, @min: @radius-min, @max: @radius-max) {
  border-radius: clamp(@min, @base, @max);
}

// Circle border radius (fixed adaptation, ensures circle doesn't deform)
.m-radius-circle() {
  border-radius: 50%;
}

// Pill border radius
.m-radius-pill() {
  border-radius: 999px;
}

// ========== Border and Shadow Adaptation ==========
// Universal hairline border (solves 1px adaptation issue)
.m-border() {
  border: 1px solid #eee;
}

// Card shadow
.m-shadow() {
  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.08);
}

// Light shadow
.m-shadow-light() {
  box-shadow: 0 1px 6px 0 rgba(0, 0, 0, 0.05);
}

// ========== Layout ==========
// Flex horizontal centering
.m-flex-center() {
  display: flex;
  justify-content: center;
  align-items: center;
}

// Flex space-between alignment
.m-flex-between() {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

// Flex vertical centering
.m-flex-col-center() {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

// Text overflow ellipsis (single line)
.m-ellipsis() {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

// Text overflow ellipsis (multi-line)
.m-ellipsis-multi(@line: 2) {
  display: -webkit-box;
  -webkit-line-clamp: @line;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

// ========== Extreme Device Fallback ==========
// Prevent scaling, fixed size (does not participate in vw adaptation)
.m-fixed() {
  flex-shrink: 0 !important;
}

// Fill remaining space
.m-flex-auto() {
  flex: 1;
  overflow: hidden;
}

Usage example:

.demo-card {
  // Width and height adaptation
  .m-width(680px);
  .m-padding(30px);
  .m-radius(16px);
  .m-shadow();

  .title {
    .m-font-base();
    .m-flex-between();
  }

  .desc {
    .m-font-sm();
    .m-ellipsis-multi(2);
    color: #666;
  }
}

One thing you must pay attention to: if you are using the Vant UI framework, its design draft base width is 375, not 750.

So if you have installed Vant, the viewportWidth in postcss.config.js can be changed to a function:

viewportWidth: (file) => {
    // vant components use 375, business code uses 750
    return file.includes(path.join("node_modules", "vant")) ? 375 : 750;
},