目录

Responsive Layout

Overview

Responsive Web Design (RWD) is a web design methodology in the field of website design. It aims to enable websites to deliver a good reading and interaction experience across different devices and screen sizes, without the need to create separate versions for each different device or screen size. The core of this design approach lies in the ability of page layouts and content to flexibly adjust according to the characteristics of the device used by the user (such as screen size, resolution, and orientation).

In HarmonyOS, the responsive design is mainly used for UI development, with the goal of ensuring that apps can provide a consistent and excellent user experience across multiple HarmonyOS-powered devices, including those with different screen sizes and resolutions. To this end, HarmonyOS provides a series of responsive layout capabilities and tools for implementing multi-device layouts.

Based on the responsive design methodology, the responsive layout method allows pages to automatically adjust their layouts to different screen sizes, thereby delivering a more comfortable UI and better user experience. The following figures show the responsive layouts.

Figure 1 Responsive layout 1
Figure 2 Responsive layout 2

This section describes the four types of responsive layout capabilities to help you implement responsive layout.

Responsive Layout Capability

Description

Breakpoints

Breakpoints divide the window width into different ranges. The responsive system listens for the window size changes and automatically adjusts the layout when a breakpoint change is detected.

Media queries

Media queries support listening for changes in various media features such as the window width, screen orientation (landscape/portrait), color mode (light/dark), and device type. The layout is adjusted accordingly when these features change.

Grid

The Grid component divides its containing area into regular columns. You can achieve various layout effects by adjusting parameters of the Grid component and the number of columns occupied by its child components across different breakpoints.

Responsive components

Some components provided by HarmonyOS support responsive layout, such as Tabs, Swiper, Grid, List, and GridRow. You can achieve different display effects by setting breakpoints.

Breakpoints

The responsive layout enables page elements to automatically adapt to changes in the external container based on specific features (such as window width or screen orientation). As the most frequently used features, the window width and height-to-width ratio are divided into different ranges (that is, breakpoints) by the system. From one breakpoint to another, the window width and height-to-width ratio change, so does the page layout (for example, the page content shifts from a single-column layout to a dual-column or even triple-column layout). This results in a better display effect.

This section describes the design principles, definition, and usage of breakpoints to help you implement responsive layout.

[h2]Design Principles of Breakpoints

Multi-device usage continuity should be considered for better all-scenario experience. You are advised to comply with the following principles to design the app page layout:

Therefore, horizontal and vertical breakpoints are designed to represent different characteristics of the window and serve as criteria for determining page layouts and interaction experiences.

For details about how to use horizontal breakpoints to implement principle 1, see Using Horizontal Breakpoints. For details about how to use horizontal and vertical breakpoints to implement principle 2, see Using Vertical Breakpoints.

[h2]Definition of Breakpoints

Horizontal breakpoints are set based on the app window width. Five ranges are recommended based on different thresholds.

Breakpoint

Window Width (vp)

xs

(0, 320)

sm

[320, 600)

md

[600, 840)

lg

[840, 1440)

xl

[1440, +∞)

Vertical breakpoints are set based on the height-to-width ratio of the app window. Three ranges are recommended based on different thresholds.

Breakpoint

Height-to-Width Ratio

sm

(0, 0.8)

md

[0.8, 1.2)

lg

[1.2, +∞)

Figure 3 Breakpoint ranges of common HarmonyOS devices
  1. The official website may not cover the breakpoint ranges of all products. Therefore, a supplementary method is provided here: You can use the display module API to obtain the screen width and height attributes and determine the horizontal and vertical breakpoint ranges of the current device.

  2. Breakpoints are designed for the window rather than the device type. Windows at the same breakpoint display the same page layout. Different window forms (such as full-screen mode, split-screen mode, and freeform window) on the same device may fall into different breakpoints and display different layouts.
  3. For smartphones and small-screen foldable phones (landscape/reverse landscape), such as Pura70 Pro and Pocket2, the horizontal breakpoint is lg.
  4. You can decide which breakpoints to adapt based on actual scenarios. For example, the xs breakpoint typically corresponds to wearables—if a page is confirmed not to be displayed on wearables, adapting to the xs breakpoint can be omitted.
  5. You can add breakpoints such as xl and xxl after the lg breakpoint based on actual scenarios.

The following table lists the full-screen window sizes and breakpoint ranges (both horizontal and vertical) supported by mainstream HarmonyOS devices.

Device Type

Product Model

Full-Screen Window Dimensions (Using the First Listed Product Model as an Example, in vp)

Horizontal Breakpoint

Vertical Breakpoint

Smartphone (portrait/reverse portrait)

Mate60, Mate60 Pro, Mate70, Mate70 Pro, Pura60, Pura60 Pro, Pura70, and Pura70 Pro

827 × 374

sm

lg

Widescreen foldable phone

Pura X

Internal screen (portrait/reverse portrait): 707 × 440

Internal screen: sm

Internal screen: lg

External screen (reverse landscape): 326 × 326

External screen: sm

External screen: md

Pura X Max

Internal screen (landscape/reverse landscape): 939 × 664

Internal screen: lg

Internal screen: sm

External screen (portrait): 459 × 672

External screen: sm

External screen: lg

Small-screen foldable phone

Pocket 2

Internal screen (portrait/reverse portrait): 860 × 364

Internal screen: sm

Internal screen: lg

External screen: /

External screen: /

External screen: /

Bi-fold phone

Mate X5 and Mate X6

Internal screen (portrait/reverse portrait): 798 × 711

Internal screen: md

Internal screen: md

External screen (portrait/reverse portrait): 801 × 345

External screen: sm

External screen: lg

Tri-fold phone

Mate XT

F state (single-screen display in portrait/reverse portrait): 776 × 350

F state (single-screen display): sm

F state (single-screen display): lg

M state (dual-screen display in portrait/reverse portrait): 776 × 712

M state (dual-screen display): md

M state (dual-screen display): md

G state (triple-screen display in landscape/reverse landscape): 1107 × 776

G state (triple-screen display): lg

G state (triple-screen display): sm

Tablet (landscape/reverse landscape)

MatePad and MatePad Pro

1137 × 711

lg

sm

PC

MateBook Pro (landscape)

1642 × 1094

xl

sm

MateBook Fold (landscape in unfolded state)

1831 × 1307

xl

sm

Vision

Mate TV

1280 × 720

lg

sm

[h2]Breakpoint-based UI Refresh

Refreshing the UI by using breakpoint environment variables

Since API version 22, you can use the @Env decorator of responsive system environment variables to read breakpoint information. When the dimensions of the window where a component is located change, the breakpoint environment variables decorated with @Env will be updated, and the components associated with the breakpoint environment variables will be updated accordingly. In this way, the UI content is updated synchronously.

After the environment variable information of the window where @Component or @ComponentV2 is located is obtained, when the dimensions of the window change, the breakpoint environment variables will be updated based on the new window dimensions and the related UI components will be updated accordingly. Common application scenarios include:

Refreshing the UI by listening for breakpoint changes

  1. Use the custom window information class WindowInfo to save the window breakpoint information.

    @Observed
    export class WindowInfo {
      // ...
      // Width/height breakpoint.
      public widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_XS;
      public heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM;
      // ...
    }

  2. Create a window information class object in the WindowUtil class and call getWindowWidthBreakpoint() and getWindowHeightBreakpoint() to obtain the horizontal and vertical breakpoints of the current window. Call on('windowSizeChange') to register a listener for window dimension changes, and re-obtain the breakpoints through the callback.

    export class WindowUtil {
      // ...
      public mainWindowInfo: WindowInfo = new WindowInfo();
      // ...
      public onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
        this.mainWindowInfo.windowSize = windowSize;
        this.mainWindowInfo.widthBp = this.uiContext!.getWindowWidthBreakpoint();
        this.mainWindowInfo.heightBp = this.uiContext!.getWindowHeightBreakpoint();
      };
      // ...
      updateWindowInfo(): void {
        try {
          // ...
          // First time get width/height breakpoint.
          this.mainWindowInfo.widthBp = this.uiContext!.getWindowWidthBreakpoint();
          this.mainWindowInfo.heightBp = this.uiContext!.getWindowHeightBreakpoint();
          // Register for window size change monitoring, update window size and width/height breakpoint.
          this.mainWindow.on('windowSizeChange', this.onWindowSizeChange);
          // ...
        } catch (error) {
          let err = error as BusinessError;
          hilog.error(0x0000, `TestLog`, `Failed to update window info. Code: ${err.code}, message: ${err.message}`);
        }
      }
      // ...
    }

  3. In the onWindowStageCreate() lifecycle of EntryAbility, obtain the app window instance, initialize WindowUtil, and save it to AppStorage. After the page content is loaded using loadContent, register breakpoint listeners and update breakpoints. After the preceding steps are complete, update the horizontal and vertical breakpoints when the window dimensions change.

    onWindowStageCreate(windowStage: window.WindowStage): void {
      // ...
      try {
        this.windowUtil = new WindowUtil(windowStage.getMainWindowSync());
      } catch (error) {
        let err = error as BusinessError;
        hilog.error(0x0000, 'TestLog', `Failed to get main window. Code: ${err.code}, message: ${err.message}`);
      }
      AppStorage.setOrCreate('windowUtil', this.windowUtil);
    
      windowStage.loadContent('pages/Index', (err) => {
        // ...
        this.windowUtil!.setUIContext();
        this.windowUtil!.setImmersiveType(ImmersiveType.IMMERSIVE);
        this.windowUtil!.updateWindowInfo();
      });
    }

  4. Use the @StorageLink decorator to obtain the WindowUtil object of the window management class. When a child component is initialized, transfer the window information class object so that the child component can obtain the breakpoint information of the current window.

    @Component
    export struct GridLayout {
      @StorageLink('windowUtil') windowUtil: WindowUtil | undefined = undefined;
      pageInfos: NavPathStack = new NavPathStack();
    
      build() {
        NavDestination() {
          GridView({
            mainWindowInfo: this.windowUtil?.mainWindowInfo,
            pageInfos: this.pageInfos
          })
        }
        // ...
      }
    }

  5. Use the @ObjectLink decorator to receive the window information class object and perform layout for different breakpoint ranges based on the obtained window information. The following sample code implements the feature of displaying different numbers of columns in the Grid component at different breakpoints.

    @Component
    export struct GridView {
      @ObjectLink mainWindowInfo: WindowInfo;
      // ...
    
      build() {
        Scroll() {
          Column() {
            // ...
            Grid() {
              // ...
            }
            .width('100%')
            .columnsTemplate(`repeat(${new WidthBreakpointType(2, 3, 4, 4).getValue(this.mainWindowInfo.widthBp)}, 1fr)`)
            .columnsGap(12)
            .rowsGap(12)
          }
          // ...
        }
        // ...
      }
    }

[h2]Using Horizontal Breakpoints

This section introduces how to use horizontal breakpoints to implement the same layout for two windows with similar widths.

To implement a cross-device app, develop design schemes based on the horizontal breakpoint sets of different device types. For example, if an app needs to run on bar-type smartphones, bi-fold phones (Mate X series), and tablets, you should design different page layouts at three horizontal breakpoints (sm, md, and lg). Below shows the layout effects.

sm

md

lg

Not all horizontal breakpoints can be involved in actual app development. You can flexibly use utility classes based on the service requirements to assign values to attributes of the responsive layout. For example, if your app only needs to adapt to bar-type smartphones, bi-fold phones, and tablets, you can use the utility class WidthBreakpointType to override the member variables at three horizontal breakpoints to implement the specific responsive layout.

export class WidthBreakpointType<T> {
  sm: T;
  md: T;
  lg: T;
  xl: T;

  constructor(sm: T, md: T, lg: T, xl: T) {
    this.sm = sm;
    this.md = md;
    this.lg = lg;
    this.xl = xl
  }

  getValue(widthBp: WidthBreakpoint): T {
    if (widthBp === WidthBreakpoint.WIDTH_XS || widthBp === WidthBreakpoint.WIDTH_SM) {
      return this.sm;
    }
    if (widthBp === WidthBreakpoint.WIDTH_MD) {
      return this.md;
    }
    if (widthBp === WidthBreakpoint.WIDTH_LG) {
      return this.lg;
    } else {
      return this.xl;
    }
  }
}

Set the font size at different breakpoints. For example, you can use BreakpointType to set the font size to 14 fp, 16 fp, and 18 fp at the sm, md, and lg horizontal breakpoints, respectively.

Text('Test')
  .fontSize(new BreakpointType('14fp', '16fp', '18fp').getValue(this.currentWidthBreakpoint))

At the sm and md horizontal breakpoints, the font size is 14 fp. At the lg breakpoint, the font size is 16 fp. Use the ternary expression to set the attribute at a specific horizontal breakpoint.

Text('Test')
  .fontSize(this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_LG ? '16fp' : '14fp')

[h2]Using Vertical Breakpoints

The following introduces how to use both the horizontal and vertical breakpoints to implement special layouts for windows with a relatively low height-to-width ratio, such as horizontal windows and square-like windows.

The following methods are recommended to determine whether a window is a horizontal window or a square-like window and whether to display special page layouts.

Window Type

Horizontal Window

Square-like Window

Effect

Condition

The vertical breakpoint is sm, or the height-to-width ratio of the window is less than 0.8.

The vertical breakpoint is md, or the height-to-width ratio of the window is within the range of [0.8, 1.2).

// Judgment of the horizontal window. (The actual application may need to be combined with other conditions, for example, determine the horizontal breakpoint)
if (this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_SM &&
  this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_MD) {
  // Horizontal window page layout.
}
// Judgment of the square window. (The actual use may need to be combined with other conditions, such as determining horizontal breakpoints)
if (this.currentHeightBreakpoint === HeightBreakpoint.HEIGHT_MD &&
  this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM) {
  // Square-like window page layout.
}

Unique Small Window Layout

To deliver a unique user experience, square-like small windows are designed with a unique layout. Common scenarios include phones in 1:1 horizontal split-screen mode and small square screens (such as the external screen of Pura X series products)—these two scenarios can be distinguished using the horizontal breakpoint sm and vertical breakpoint md, as shown in the following figure. For more details and sample code, see Pura X Application Development.

Other Special Scenarios

In addition to the unique small-window layout and square-like screen rotation solution, there are some special scenarios where horizontal and vertical breakpoints should be used together for determination.

The following uses the full-screen playback page of a video app as an example. For bar-type smartphones in landscape mode, windows cannot be rotated. For bi-fold phones in unfolded state or tablets in portrait mode, windows can be rotated. The horizontal breakpoints in these three scenarios are within the md range and cannot be distinguished. In this case, use both the horizontal and vertical breakpoints, enabling the window rotation solution of the full-screen playback to adapt to multiple devices.

  1. Ensure that the initialization operations in steps 5 and 6 in Breakpoint-based UI Refresh have been completed.
  2. Use the @Watch decorator to listen for the change of the state variable isFullScreen to determine whether the video is played in full-screen mode. The window orientation is adjusted accordingly as the window display mode (full-screen or windowed) changes. When the video is played in full-screen mode, the orientation of the window without breakpoints should be set to AUTO_ROTATION_LANDSCAPE for smartphones and bi-fold phones (X series) in the folded and semi-folded states.

    Figure 4 Effect on a smartphone
    Figure 5 Effect on a bi-fold phone (X series) in semi-folded state

    Negative example:

    if (this.isFullScreen) {
      if (deviceInfo.deviceType !== '2in1') {
        this.windowUtil!.disableWindowSystemBar();
      }
      try {
        if ((!display.isFoldable() && deviceInfo.deviceType === 'phone') ||
          display.getFoldStatus() === display.FoldStatus.FOLD_STATUS_FOLDED) {
          this.windowUtil!.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE);
        }
        if (display.isFoldable()) {
          if (this.isHalfFolded) {
            this.windowUtil!.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE);
          }
        }
      } catch (error) {
        let err = error as BusinessError
        hilog.error(0x0000, 'VideoExample',
          `setMainWindowOrientation failed. code=${err.code}, message = ${err.message}`);
      }
    }

    According to the preceding code, isFoldable, deviceType, and getFoldStatus are used to determine whether users access the full-screen mode. This method has poor readability and maintainability. As a result, exceptions may occur on different devices with HarmonyOS ecosystem expansion.

    Positive example:

    • Bi-fold phones (X series) in unfolded state and tablets both support rotation. For bi-fold phones (X series), the horizontal and vertical breakpoints are both md. For tablets in landscape mode, the horizontal breakpoint is lg, and for tablets in portrait mode, the horizontal breakpoint is md, and the vertical breakpoint is lg. In this case, set the window display orientation to AUTO_ROTATION_RESTRICTED.
    • For smartphones and folded bi-fold phones (X series) in portrait mode, the horizontal breakpoint is sm and the vertical breakpoint is lg. Horizontal rotation is supported during full-screen playback. In this case, set the window display orientation to AUTO_ROTATION_LANDSCAPE_RESTRICTED.
    • For smartphones and folded bi-fold phones (X series) in landscape mode, the horizontal breakpoint is md and the vertical breakpoint is sm. If the video exits the full-screen mode, the portrait layout is displayed. In this case, set the window display orientation to PORTRAIT.

      This case does not account for scenarios where the horizontal breakpoint falls into lg when certain smartphone models (such as Pocket 2 and Pura 70 Pro) are in landscape mode.

        // entry/src/main/ets/page/VideoExample.ets
        private onFullScreenChange(): void {
          // Large folding screen (X series) in unfolded state and tablet state, supporting rotation && large folding screen (X series) in hover state requires landscape display and does not support rotation.
          if (((this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_MD && this.currentHeightBreakpoint !==
          HeightBreakpoint.HEIGHT_SM) || this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_LG) &&
            !this.isHalfFolded) {
            this.windowUtil?.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED);
          }
          // Phone and large folding screen (X series) in portrait mode.
          else if (this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_SM && this.currentHeightBreakpoint ===
          HeightBreakpoint.HEIGHT_LG) {
            // In full-screen mode, the layout is displayed in landscape mode. Otherwise, the layout is displayed in portrait mode.
            if (this.isFullScreen) {
              this.windowUtil?.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_LANDSCAPE_RESTRICTED);
            } else {
              this.windowUtil?.setMainWindowOrientation(window.Orientation.PORTRAIT);
            }
          }
          // When the mobile phone and large folding screen (X series) are folded in landscape mode and the playback is not in full screen mode, the vertical display layout is displayed.
          else if (this.currentWidthBreakpoint === WidthBreakpoint.WIDTH_MD && this.currentHeightBreakpoint ===
          HeightBreakpoint.HEIGHT_SM && !this.isFullScreen) {
            this.windowUtil?.setMainWindowOrientation(window.Orientation.PORTRAIT);
          }
          // The navigation bar is not hidden on a 2in1 device.
          if (deviceInfo.deviceType !== '2in1') {
            // The navigation bar is hidden in full-screen playback. Otherwise, the navigation bar is displayed.
            if (this.isFullScreen) {
              this.windowUtil!.disableWindowSystemBar();
            } else {
              this.windowUtil!.enableWindowSystemBar();
            }
          }
        }
      }

By using the vertical and horizontal breakpoints, you can use only one set of code to implement multi-device compatibility of the full-screen playback page. In actual development, you can use this solution for other similar projects.

Media Query

In practical app development, you often need to modify the app style for different types of devices or different states of the same type of device. Media query provides rich capabilities to listen for media features, covering changes in the app display area, orientation, light/dark mode, and device type, making them widely used in app development.

This section describes how to use media query to listen for breakpoint changes. For details, see Media Query.

Example

Obtain the breakpoint value of the current app by listening for the app window width changes through media queries.

sm

md

lg

  1. Implement a simple encapsulation for breakpoint listening via media queries to facilitate reuse.
    import { mediaquery } from '@kit.ArkUI';
    
    export type BreakpointType = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
    
    export interface Breakpoint {
      name: BreakpointType
      size: number
      mediaQueryListener?: mediaquery.MediaQueryListener
    }
    
    export class BreakpointSystem {
      private static instance: BreakpointSystem;
      private readonly breakpoints: Breakpoint[] = [
        { name: 'xs', size: 0 },
        { name: 'sm', size: 320 },
        { name: 'md', size: 600 },
        { name: 'lg', size: 840 }
      ]
      private states: Set<BreakpointState<Object>>;
    
      private constructor() {
        this.states = new Set();
      }
    
      public static getInstance(): BreakpointSystem {
        if (!BreakpointSystem.instance) {
          BreakpointSystem.instance = new BreakpointSystem();
        }
        return BreakpointSystem.instance;
      }
    
      public attach(state: BreakpointState<Object>): void {
        this.states.add(state);
      }
    
      public detach(state: BreakpointState<Object>): void {
        this.states.delete(state);
      }
    
      public start() {
        this.breakpoints.forEach((breakpoint: Breakpoint, index) => {
          let condition: string;
          if (index === this.breakpoints.length - 1) {
            condition = `(${breakpoint.size}vp<=width)`;
          } else {
            condition = `(${breakpoint.size}vp<=width<${this.breakpoints[index + 1].size}vp)`;
          }
          let uiContext = AppStorage.get('uiContext') as UIContext;
          breakpoint.mediaQueryListener = uiContext.getMediaQuery().matchMediaSync(condition);
          if (breakpoint.mediaQueryListener.matches) {
            this.updateAllState(breakpoint.name);
          }
          breakpoint.mediaQueryListener.on('change', (mediaQueryResult) => {
            if (mediaQueryResult.matches) {
              this.updateAllState(breakpoint.name);
            }
          });
        })
      }
    
      private updateAllState(type: BreakpointType): void {
        this.states.forEach(state => state.update(type));
      }
    
      public stop() {
        this.breakpoints.forEach((breakpoint: Breakpoint) => {
          if (breakpoint.mediaQueryListener) {
            breakpoint.mediaQueryListener.off('change');
          }
        })
        this.states.clear();
      }
    }
    
    export interface BreakpointOptions<T> {
      xs?: T;
      sm?: T;
      md?: T;
      lg?: T;
      xl?: T;
      xxl?: T;
    }
    
    export class BreakpointState<T extends Object> {
      public value: T | undefined = undefined;
      private options: BreakpointOptions<T>;
    
      constructor(options: BreakpointOptions<T>) {
        this.options = options;
      }
    
      static of<T extends Object>(options: BreakpointOptions<T>): BreakpointState<T> {
        return new BreakpointState(options);
      }
    
      public update(type: BreakpointType): void {
        if (type === 'xs') {
          this.value = this.options.xs;
        } else if (type === 'sm') {
          this.value = this.options.sm;
        } else if (type === 'md') {
          this.value = this.options.md;
        } else if (type === 'lg') {
          this.value = this.options.lg;
        } else if (type === 'xl') {
          this.value = this.options.xl;
        } else if (type === 'xxl') {
          this.value = this.options.xxl;
        } else {
          this.value = undefined;
        }
      }
    }
  2. Obtain the breakpoint value of the current app in the page by listening for the app window width changes through media queries.
    import { BreakpointSystem, BreakpointState } from './Breakpoint';
    
    @Entry
    @Component
    struct BreakpointSample {
      @State compStr: BreakpointState<string> = BreakpointState.of({ sm: 'sm', md: 'md', lg: 'lg' })
      @State compImg: BreakpointState<Resource> = BreakpointState.of({
        sm: $r('app.media.sm_new'),
        md: $r('app.media.md_new'),
        lg: $r('app.media.lg_new')
      });
    
      aboutToAppear() {
        BreakpointSystem.getInstance().attach(this.compStr);
        BreakpointSystem.getInstance().attach(this.compImg);
        BreakpointSystem.getInstance().start();
      }
    
      aboutToDisappear() {
        BreakpointSystem.getInstance().detach(this.compStr);
        BreakpointSystem.getInstance().detach(this.compImg);
        BreakpointSystem.getInstance().stop();
      }
    
      build() {
        Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) {
          Column()
            .height(100)
            .width(100)
            .backgroundImage(this.compImg.value)
            .backgroundImagePosition(Alignment.Center)
            .backgroundImageSize(ImageSize.Contain)
    
          Text(this.compStr.value)
            .fontSize(24)
            .margin(10)
        }
        .width('100%')
        .height('100%')
      }
    }

Grid

The grid system is a two-dimensional grid structure used for page layout, serving as a common auxiliary positioning tool for multi-device design. It divides the page horizontally into columns of equal width, and enables flexible layouts by setting the number of columns occupied by the content.

By using grids, the design and development costs for adapting to different screen sizes are significantly reduced, and the overall design and development process is more orderly and rhythmic. In addition, the responsive grid offers a consistent display experience across devices.

The grid system of HarmonyOS employs a 12-column design. As 12 is divisible by 2, 3, 4, and 6, it offers greater layout flexibility. The grid layout has the following advantages:

  1. Traceable pattern: The grid layout can provide a regular structure for the layout, helping resolve the dynamic layout issue for multiple sizes and devices. By dividing a page into columns and rows of the equal width, the grid layout helps you easily locate and typeset the UI elements.
  2. Unified positioning reference: The grid layout can provide a unified positioning reference for the system, ensuring layout consistency of each module across different devices. This helps reduce the complexity of design and development and improve work efficiency.
  3. Flexible spacing adjustment method: The grid layout can provide a flexible spacing adjustment method to meet the requirements of layout adjustment in special scenarios. By adjusting the spacing between columns and between rows, you can control the overall page layout.
  4. Auto-wrap and self-adaptation: The grid layout can implement auto-wrap and self-adaptation for one-to-many layouts. When the number of page elements exceeds the capacity of a row or column, they automatically wrap to the next row or column and adaptively adjust their layout across different devices. This makes the page layout more flexible and adaptable.
Figure 6 Grid layout

The grid style is determined by the margin, gutter, and columns properties.

The width of a single column is automatically calculated by the system by combining margin, gutter, and columns — manual configuration by developers is neither needed nor allowed.

The following uses the grid width of a smartphone in portrait mode as an example:

Screen width: 360 vp

Margin: 24 vp

Gutter: 24 vp

Number of grids: 4 (including three gutters)

Calculation formula:

Width of one grid = (Screen width – Margin × 2 – Gutter × 3)/4 = (360 – 24 × 2 – 24 × 3)/4 = 60 (vp)

Width of two grids = Width of one grid + Gutter + Width of one grid = 60 + 24 + 60 = 144 (vp)

Width of three grids = Width of two grids + Gutter + Width of one grid = 144 + 24 + 60 = 228 (vp)

Width of four grids = Width of three grids + Gutter + Width of one grid = 228 + 24 + 60 = 312 (vp)

Based on comprehensive weighing of the screen features on various devices, using 8 vp as the basic unit of the grid allows for better planning of the size, position, and alignment of UI elements. This helps build layouts with a stronger sense of hierarchy and order, and consistent effects across multiple devices. Some small components (such as icons) can also have their sizes aligned with the 4 vp grid. Values for Margin and Gutter in grid layouts are set with reference to the 8 vp/4 vp grid system. For example, in grid designs for smartphone devices: the base grid margin is 24 vp (that is, 3 × 8 vp); the card grid margin is 12 vp (that is, 8 vp + 4 vp).

The following table lists the number of grids, margin, and gutter of each device.

Parameter

Smartphone

Foldable Phone

Tablet

IVI

Vision

Number of grids in portrait mode

4

8

8

-

-

Number of grids in landscape mode

8

8

12

12

12

Base grid margin (vp)

24

24

24

24

48

Base grid gutter (vp)

24

24

24

24

24

Card grid margin (vp)

12

12

12

12

48

Card grid gutter (vp)

12

12

12

12

24

The grid layout integrates breakpoints, and the components that implement grid layout capabilities are called grid components. In practical scenarios, you can configure the number of columns occupied by elements in a grid component for different breakpoints, while also adjusting values of Margin, Gutter, and Columns to achieve different layout effects.

sm

md

  • In API Version 9, ArkUI refactored the grid components, introducing new GridRow and GridCol components while deprecating the original GridContainer component and grid configurations.
  • Unless otherwise specified, all references to grid components in this document refer to the GridRow and GridCol components.

[h2]Breakpoints of Grid Components

Grid components offer comprehensive breakpoint customization capabilities, allowing you to tailor layouts across diverse screen sizes.

(1) You can modify the breakpoint value ranges and configure up to six breakpoints.

Example 1

Modify the default breakpoint value ranges and enable the xl and xxl breakpoints.

The lower left corner of the figure shows the screen size of the current device (also the app window size). As the window size changes, the breakpoints of the grid components change accordingly. (For ease of understanding, the device DPI is set to 160 in the figure, where 1 vp equals 1 px.)

@Entry
@Preview
@Component
struct GridRowSample1 {
  @State currentBreakpoint: string = 'md';

  build() {
    GridRow({ breakpoints: { value: ['320vp', '600vp', '840vp', '1440vp'] } }) {
      GridCol({
        span: {
          xs: 12,
          sm: 12,
          md: 12,
          lg: 12,
          xl: 12
        }
      }) {
        Row() {
          Text(this.currentBreakpoint)
            .fontSize(100)
            .fontWeight(FontWeight.Bolder)
        }
        .width('100%')
        .height('100%')
        .alignItems(VerticalAlign.Center)
        .justifyContent(FlexAlign.Center)
      }
    }
    .onBreakpointChange((breakPoint: string) => {
      this.currentBreakpoint = breakPoint;
    })
  }
}

(2) Grid breakpoints take the window width as the reference by default, but you can also configure them to reference the grid component's own width.

Grids can be applied to both overall page layouts and local page layouts. For scenarios where the app window size remains constant while specific regions within the layout resize, grid components that reference their own width for breakpoint responsiveness deliver superior flexibility.

Example 2

This example describes responding to breakpoint changes with reference to the grid component's own width. This applies to scenarios where the window size remains unchanged, but certain content areas need to undergo responsive changes.

For clarity, the device screen width of the custom previewer is set to 650 vp. The sample code constrains the sidebar's width to [100 vp, 600 vp], causing the width of the grid component on the right to adjust correspondingly between [550 vp, 50 vp]. As configured in the code, the grid component width changes trigger breakpoint transitions when the grid component crosses defined thresholds.

@Entry
@Component
struct GridRowSample2 {
  @State currentBreakpoint: string = 'md';

  build() {
    // Users can adjust the width of the sidebar and content area by dragging the divider in the sidebar component.
    SideBarContainer(SideBarContainerType.Embed) {
      // Sidebar, with a resizable range of [100vp, 600vp].
      Column() {
      }
      .width('100%')
      .backgroundColor($r('sys.color.comp_background_secondary'))

       // Content area.
      GridRow({
        breakpoints: {
          value: ['320vp', '600vp', '840vp', '1440vp'],
          reference: BreakpointsReference.ComponentSize
        }
      }) {
        GridCol({
          span: {
            xs: 12,
            sm: 12,
            md: 12,
            lg: 12,
            xl: 12
          }
        }) {
          Row() {
            Text(this.currentBreakpoint)
              .fontSize(50)
              .fontWeight(FontWeight.Bolder)
          }
          .width('100%')
          .height('100%')
          .justifyContent(FlexAlign.Center)
          .alignItems(VerticalAlign.Center)
        }
      }
      .onBreakpointChange((currentBreakpoint: string) => {
        this.currentBreakpoint = currentBreakpoint;
      })
      .width('100%')
    }
    // The sidebar does not auto-hide when dragged to its minimum width.
    .autoHide(false)
    .sideBarWidth(100)
    // Minimum width of the sidebar.
    .minSideBarWidth(100)
    // Maximum width of the sidebar.
    .maxSideBarWidth(600)
  }
}

(3) When the breakpoints of a grid component change, developers will be notified via the onBreakPointChange event.

For details about how to use the onBreakpointChange event, see the description in preceding examples.

[h2]columns, gutter, and margin of the Grid Component

The columns and gutter properties of the grid component default to 12 and 0, respectively. You can also customize these properties for different breakpoints as required. Notably, when the total columns occupied by multiple elements exceed the grid's total column count, content wrapping occurs. The grid component also allows separate definitions for the horizontal gutter (spacing between adjacent columns) and the vertical gutter (spacing between adjacent wrapped rows).

Since margin and padding are already available as universal component properties, the grid component does not provide additional margin properties. Use universal properties directly. Both the margin and padding properties can control the distance between the grid component and the left/right edges of its parent container. However, there are key differences:

In summary, margin is external to the component while padding is internal. You can choose based on specific layout requirements to achieve the desired effect.

Example 3

Define columns and gutter at different breakpoints.

sm

md

lg

@Entry
@Component
struct GridRowSample3 {
  private bgColors: ResourceColor[] = [
    $r('sys.color.ohos_id_color_palette_aux1'),
    $r('sys.color.ohos_id_color_palette_aux2'),
    $r('sys.color.ohos_id_color_palette_aux3'),
    $r('sys.color.ohos_id_color_palette_aux4'),
    $r('sys.color.ohos_id_color_palette_aux5'),
    $r('sys.color.ohos_id_color_palette_aux6')
  ];

  build() {
    // Config the values of columns and gutter at different breakpoints.
    GridRow({
      columns: { sm: 4, md: 8, lg: 12 },
      gutter: {
        x: { sm: 8, md: 16, lg: 24 },
        y: { sm: 8, md: 16, lg: 24 }
      }
    }) {
      ForEach(this.bgColors, (bgColor: ResourceColor) => {
        GridCol({ span: { sm: 2, md: 2, lg: 2 } }) {
          Row()
            .width('100%')
            .backgroundColor(bgColor)
            .height(30)
        }
      }, (bgColor: ResourceColor) => bgColor.toString())
    }
  }
}

Example 4

Both margin and padding universal properties can control the distance between the grid component and the left/right edges of its parent container, with the critical distinction being that padding is calculated within the grid's boundary while margin is calculated outside the boundary. Additionally, the onBreakpointChange event allows modifying margin or padding values at different breakpoints.

@Entry
@Component
struct GridRowSample4 {
  @State gridMargin: number = 0;

  build() {
    Column() {
      Row()
        .width('100%')
        .height(30)

      // Control the left and right spacing of the grid using padding.
      GridRow() {
        GridCol({ span: { sm: 12, md: 12, lg: 12 } }) {
          Row() {
            Text('padding')
              .fontSize(24)
              .fontWeight(FontWeight.Medium)
          }
          .width('100%')
          .height('100%')
          .alignItems(VerticalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .backgroundColor($r('sys.color.comp_background_secondary'))
        }
      }
      .height(50)
      .borderWidth(2)
      .borderColor('#F1CCB8')
      .padding({
        left: this.gridMargin,
        right: this.gridMargin
      })
      // Configure the left and right spacing values of grid components at different breakpoints using breakpoint change events.
      .onBreakpointChange((currentBreakpoint: string) => {
        if (currentBreakpoint === 'xs' || currentBreakpoint === 'sm') {
          this.gridMargin = 12;
        } else {
          this.gridMargin = 24;
        }
      })

      Row()
        .width('100%')
        .height(30)

      // Control the left and right spacing of the grid using margin.
      GridRow() {
        GridCol({ span: { sm: 12, md: 12, lg: 12 } }) {
          Row() {
            Text('margin')
              .fontSize(24)
              .fontWeight(FontWeight.Medium)
          }
          .width('100%')
          .height('100%')
          .alignItems(VerticalAlign.Center)
          .justifyContent(FlexAlign.Center)
          .backgroundColor($r('sys.color.comp_background_secondary'))
        }
      }
      .height(50)
      .borderWidth(2)
      .borderColor('#F1CCB8')
      .margin({
        left: this.gridMargin,
        right: this.gridMargin
      })
    }
  }
}

[h2]span, offset, and order of the Grid Component

The direct child node of the GridRow component can only be the GridCol component, which supports the span, offset, and order parameters. The values of these three parameters are inherited in the backward direction (xs -> sm -> md -> lg -> xl -> xxl), with no support for forward inheritance. For example, if span at the sm breakpoint is set to 3 and not configured for the md breakpoint, the md breakpoint will also use a span value of 3.

Parameter

Type

Mandatory (Yes/No)

Default Value

Description

span

{xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number}

Yes

-

Number of columns occupied within a grid. If it is set to 0, the element is not involved in layout calculation, that is, the element is not rendered.

offset

{xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number}

No

0

Number of offset columns relative to the previous child component of the grid.

order

{xs?: number, sm?: number, md?: number, lg?: number, xl?: number, xxl?:number}

No

0

Sequence number of the element. Child components of the grid are sorted in ascending order based on their sequence numbers.

Example 5

Use the span parameter to configure the number of columns occupied by GridCol at different breakpoints. If you set span of elements 3 and 6 to 0 at the md breakpoint, they will not be rendered or displayed at this breakpoint.

sm

md

lg

@Entry
@Component
struct GridRowSample5 {
  private elements: ColorArr[] = [
    { index: 1, color: $r('sys.color.ohos_id_color_palette_aux1') },
    { index: 2, color: $r('sys.color.ohos_id_color_palette_aux2') },
    { index: 3, color: $r('sys.color.ohos_id_color_palette_aux3') },
    { index: 4, color: $r('sys.color.ohos_id_color_palette_aux4') },
    { index: 5, color: $r('sys.color.ohos_id_color_palette_aux5') },
    { index: 6, color: $r('sys.color.ohos_id_color_palette_aux6') }
  ];

  build() {
    GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
      ForEach(this.elements, (item: ColorArr) => {
        GridCol({ span: { sm: 6, md: item.index % 3 === 0 ? 0 : 4, lg: 3 } }) {
          Row() {
            Text(`${item.index}`)
              .fontSize(24)
          }
          .justifyContent(FlexAlign.Center)
          .backgroundColor(item.color)
          .height(30)
          .width('100%')
        }
      }, (item: ColorArr, index: number) => (item.index + index).toString())
    }
  }
}

Example 6

Set the offset parameter to specify the number of columns between GridCol and its previous sibling component.

sm

md

lg

@Entry
@Component
struct GridRowSample6 {
  private elements: ColorArr[] = [
    { index: 1, color: $r('sys.color.ohos_id_color_palette_aux1') },
    { index: 2, color: $r('sys.color.ohos_id_color_palette_aux2') },
    { index: 3, color: $r('sys.color.ohos_id_color_palette_aux3') },
    { index: 4, color: $r('sys.color.ohos_id_color_palette_aux4') },
    { index: 5, color: $r('sys.color.ohos_id_color_palette_aux5') },
    { index: 6, color: $r('sys.color.ohos_id_color_palette_aux6') }
  ];

  build() {
    GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
      ForEach(this.elements, (item: ColorArr) => {
        GridCol({
          span: { sm: 6, md: 4, lg: 3 },
          offset: { sm: 0, md: 2, lg: 1 }
        }) {
          Row() {
            Text(`${item.index}`)
              .fontSize(24)
          }
          .justifyContent(FlexAlign.Center)
          .backgroundColor(item.color)
          .height(30)
          .width('100%')
        }
      }, (item: ColorArr, index: number) => (item.index + index).toString())
    }
  }
}

Example 7

Use the order property to control the sequence of GridCol components. At the sm and md breakpoints, GridCol components are displayed in order from 1 to 6; at the lg breakpoint, they are displayed in order from 6 to 1.

sm

md

lg

@Entry
@Component
struct GridRowSample7 {
  private elements: ColorArr[] = [
    { index: 1, color: $r('sys.color.ohos_id_color_palette_aux1') },
    { index: 2, color: $r('sys.color.ohos_id_color_palette_aux2') },
    { index: 3, color: $r('sys.color.ohos_id_color_palette_aux3') },
    { index: 4, color: $r('sys.color.ohos_id_color_palette_aux4') },
    { index: 5, color: $r('sys.color.ohos_id_color_palette_aux5') },
    { index: 6, color: $r('sys.color.ohos_id_color_palette_aux6') }
  ];

  build() {
    GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
      ForEach(this.elements, (item: ColorArr) => {
        GridCol({
          span: { sm: 6, md: 4, lg: 3 },
          order: { lg: (6 - item.index) }
        }) {
          Row() {
            Text(`${item.index}`)
              .fontSize(24)
          }
          .justifyContent(FlexAlign.Center)
          .backgroundColor(item.color)
          .height(30)
          .width('100%')
        }
      }, (item: ColorArr, index: number) => (item.index + index).toString())
    }
  }
}

Example 8

When only the span, offset, and order values are configured for sm and lg breakpoints, the md breakpoint inherits these values from sm via forward inheritance (sm -> md -> lg).

sm

md

lg

@Entry
@Component
struct GridRowSample8 {
  private elements: ColorArr[] = [
    { index: 1, color: $r('sys.color.ohos_id_color_palette_aux1') },
    { index: 2, color: $r('sys.color.ohos_id_color_palette_aux2') },
    { index: 3, color: $r('sys.color.ohos_id_color_palette_aux3') },
    { index: 4, color: $r('sys.color.ohos_id_color_palette_aux4') },
    { index: 5, color: $r('sys.color.ohos_id_color_palette_aux5') },
    { index: 6, color: $r('sys.color.ohos_id_color_palette_aux6') }
  ];

  build() {
    GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
      ForEach(this.elements, (item: ColorArr) => {
        // If the values of the three parameters are not configured at the md breakpoint, they will inherit the values from the sm breakpoint.
        GridCol({
          span: { sm: 4, lg: 3 },
          offset: { sm: 2, lg: 1 },
          order: { sm: (6 - item.index), lg: item.index }
        }) {
          Row() {
            Text(`${item.index}`)
              .fontSize(24)
          }
          .justifyContent(FlexAlign.Center)
          .backgroundColor(item.color)
          .height(30)
          .width('100%')
        }
      }, (item: ColorArr, index: number) => (item.index + index).toString())
    }
  }
}

[h2]Nesting of Grid Components

Grid components can be contained in other grid components.

Example 9

sm

md

lg

@Entry
@Component
struct GridRowSample9 {
  private elements: ColorArr[] = [
    { index: 1, color: $r('sys.color.ohos_id_color_palette_aux1') },
    { index: 2, color: $r('sys.color.ohos_id_color_palette_aux2') },
    { index: 3, color: $r('sys.color.ohos_id_color_palette_aux3') },
    { index: 4, color: $r('sys.color.ohos_id_color_palette_aux4') },
    { index: 5, color: $r('sys.color.ohos_id_color_palette_aux5') },
    { index: 6, color: $r('sys.color.ohos_id_color_palette_aux6') }
  ];

  build() {
    GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
      GridCol({
        span: { sm: 12, md: 10, lg: 8 },
        offset: { sm: 0, md: 1, lg: 2 }
      }) {
        GridRow({ columns: { sm: 12, md: 12, lg: 12 } }) {
          ForEach(this.elements, (item: ColorArr) => {
            GridCol({ span: { sm: 6, md: 4, lg: 3 } }) {
              Row() {
                Text(`${item.index}`)
                  .fontSize(24)
              }
              .justifyContent(FlexAlign.Center)
              .backgroundColor(item.color)
              .height(30)
              .width('100%')
            }
          }, (item: ColorArr, index: number) => JSON.stringify(item) + index)
        }
        .backgroundColor($r('sys.color.comp_background_secondary'))
        .height('100%')
      }
    }
  }
}

Responsive Components

Some components provided by HarmonyOS support responsive layout, such as Tabs, Swiper, Grid, List, and GridRow. You can achieve different display effects by setting breakpoints.

For details about the implementation principles and development procedure of responsive components, see Component Layout.

Responsive Layout Styles

Responsive layouts work well with the screen features of different devices and can be used if adaptive layouts cannot meet the screen experience requirements of different devices. Common responsive layouts include column layout, repetition layout, orientation layout, and indentation layout.

[h2]Column Layout

This layout allows the navigation bar and content area to be displayed in left-right mode on a wide screen, for example, the settings home page and settings details page.

When the horizontal width is greater than or equal to 600 vp, the column layout is displayed.

[h2]Repetition Layout

This layout allows you to arrange the components with the same attributes horizontally in parallel on a wide screen.

The repetition layout applies to images and combinations that are sensitive to the aspect ratio when the original image is scaled by more than 150% during content scaling.

[h2]Orientation Layout

This layout changes the content orientation from top-bottom to left-right or vice versa.

The orientation layout is applicable to screen orientation changes and the scenario that requires content integrity while the aspect ratio changes significantly (greater than 200%).

For example, the image and text arranged vertically (top to bottom) in the portrait mode switch to a horizontal (left to right) layout when the screen is in landscape mode.

[h2]Indentation Layout

This layout reveals different indentation effects on devices with different screen widths to better display content on a wide screen.

This layout is applicable to the text-only, image above text, and widget layouts. Under the corresponding grid specifications, the indentation rules are implemented through occupying a certain number of grids for layout.

If the grid is 8 columns or 12 columns, the 6-column or 8-column indentation layout can be used.

For UX design of responsive layouts, see Responsive Layout Methods.

For details about the implementation principles and development procedure of responsive layout styles, see Page Layout.

FAQs

[h2]What Are the Common Scenarios That Trigger Breakpoint Changes?

Symptom

Breakpoints change in certain scenarios.

Solution

Breakpoints are changed in the following scenarios:

[h2]Impact of Display Scaling on Breakpoints

Symptom

Changing the display scaling ratio will affect the page layout.

Possible causes

The formula for calculating vp from physical pixels (px) is as follows: vp = px/(DPI/160).

When the user changes the scaling ratio, DPI will change accordingly, which in turn causes the vp to change. You need to consider the impact of vp changes on breakpoint ranges.

The following table uses the Mate 60 as an example to describe how DPI and vp change after the display scaling ratio is changed.

Display Zoom Mode

DPI

vp (Width)

Small

2.7625

440

Default

3.25

374

Large

3.8

319

As a result, the breakpoints before and after the display scaling change may be inconsistent.

Solution

To enable your app to refresh the layout as DPI changes, use on('densityUpdate') to listen for changes in screen pixel density, re-fetch breakpoints through the listener, and refresh the UI.

To prevent display zoom from affecting the app layout, configure the density handling logic using setDefaultDensityEnabled(). The value true indicates that the app uses the system default density with fixed window layout regardless of display size changes; false indicates that the app does not use the system default density, so that windows dynamically re-layout when display size changes.

[h2]Pixel Unit Conversion

Symptom

In app development, pixel conversion is a common scenario, which requires a common pixel conversion solution.

Solution

The common pixel units are as follows:

Screen pixel unit: px, which is the physical pixel unit of the screen. 1 px indicates a pixel on the screen.

Virtual pixel unit: vp. Pixels in this unit are converted into physical pixels of the screen based on the screen pixel density. This unit is used for values whose unit is not specified. The ratio of vp to px is subject to the screen pixel density.

Font pixel unit: fp. By default, the font pixel size is the same as the virtual pixel size. That is, 1 fp = 1 vp. If users select a larger font size in the settings, the actual font size is the virtual pixel size multiplied by the scale coefficient. That is, 1 fp = 1 vp × scale.

For details about the conversion between common pixel units, see vp2px/px2vp/fp2px/px2fp/lpx2px/px2lpx.

[h2]How Do I Distinguish Between SmartPhones, Foldable Phones, Tablets, PCs, Visions, and Wearables?

Solution