UI Layout Guide

User-facing software needs the ability to structure text and interactive content in a coherent manner.

User interfaces should strive to signal the intent of a page to the user, provide familiar design languages for easier accessibility, and aim to minimise friction in completing the user's task.

From a technical standpoint, positioning interactive components, text, and images in a structured layout is straightforward. However the wide range of screen resolutions and aspect ratios possible for a desktop application, and differing usage patterns or input method can quickly turn a simple layout into a daunting task.

By building modular UI 'components' and wrapping them in an interactive layout system, an application's skeleton can be quickly designed by describing how the layout should appear on various devices.

Grid Layouts

Most layouts can be represented as blocks of content in a rectangular grid pattern. This layout approach has been dominant across print media, the web, and most productivity software. Complexity of the layout is managed by nesting rectangular grid layouts inside each other.

Grid layouts in print and computer software

Grid Fundamentals

Grid layouts have some common fundamental properties:

  • The area in which the layout lives, like a page in a book, browser context, or application window,
  • Margins from the edge of the containing area, and the content
  • Columns,
  • Blocks of content which use whole column or are separated into rows,
  • Alignment of content relative to it's parent column or area

Defining layouts with centralised control over these properties builds a consistent appearance with minimal boilerplate. The separation of content from layout improves modularity, and allows a layout to fluidly respond to a change in window size by switching to a more suitable ruleset.

Regardless of the designer's intent, different user hardware means that a layout might be forced to operate as a touch-interface on a small convertible tablet, or as a dashboard on a widescreen TV. Naive scaling of a layout doesn't work for large changes in view-port size or aspect ratio, so we use Breakpoints to step between different layout styles based on device properties.

Planning an application layout

Try to estimate how what content needs to be displayed and how often a user will use those items. Group similar components to provide logical sections.

If a given action is expected to occur often, it may be wise to anchor that component or text to a fixed bar at the edge of the view-port for fast access. Footers are commonly used for mode summaries or status information, while sidebars or top-menus are used for navigation or control.

While the global application structure might contain persistent values or interactive components, the bulk of content should be grouped logically and displayed clearly. Plan the layout of these views as separate grids from the main application structure, as modular layouts are much easier to re-arrange as a design matures.

Choosing the number of columns is likely one of the first design choices for a content grid, and should be based on the amount of horizontal area. However consider that the same layout can be achieved with different levels of nesting, and by using 'deeper' nested layouts you will find it easier to modify the design later.

Consider nesting grids, example shows a 2x3 and 1x3 of nested 2x1 cells

In general, consider the following guidelines:

  • Each area should have a purpose, and aims to communicate that to the user,
  • Focus on the content, remove unnecessary UI components or text where possible,
  • Using icons, diagrams or interactive visualisations will communicate context faster than text or charts,
  • Use contrast, colour, shape and layout to distinguish different items,

Sketching a layout on paper or in design software can be a useful prototyping step to understand how components might fit together.

Fractional Units

Sizing of content with css has been historically achieved with absolute px pixel or pt point sizes, relative sizes like em and rem, and percentages vw, vh and %. As responsive design principles are now commonplace, high DPI displays are gaining traction, many layouts often needed to perform maths to correctly size content for a given display.

For a layout with a series of columns, the minimum width of content in each column forces the maximum number of columns which can be drawn to screen. Calculating the width of each column then requires an understanding of the view-port width, column width, and total column count.

Fractional units simplify these kinds of layout issues, where 1fr is one fraction of the view-port's size. A layout with equal column spacing can be defined as 1fr 1fr 1fr or repeat(3, 1fr), and wider column sizing can be defined by using larger fractional units.

1fr alone takes full width, Three 1fr columns take 1/3rd each, 2fr is the same size as two 1fr columns

Fractional units are happy being used alongside absolute or relative sizing units, so its also acceptable to specify a column in pixels px then use fr for the remaining columns.

Breakpoints

Deciding when to alter the layout to better accomodate a viewport size is managed using the concept of predefined viewports, which help define the specific conditions which drive changes to the layout.

Breakpoint rules can be formed using a combination of minimum or maximums for width, height, aspect ratio, device resolution and device orientation.

Breakpoints can be driven by aspect ratio, width/height, or if a device is horizontal or vertical

If breakpoints are used to control layout behaviour, the interface will respond dynamically when the user resizes the window or turns their phone sideways.

By default, the provided breakpoints trigger based on window width, ranging from extra-small xs to extra large xl.

xssmmdlgxl
< 576px≥ 576px≥ 768px≥ 992px≥ 1200px

Additional breakpoints can be manually specified, see the Layout API reference for details.

Atomic Layout

atomic-layout implements a grid-layout design pattern in a type-safe and functional manner. It provides a few primitive components which underpin the layout, allows customisation overrides and name-spaced areas.

Composition provides the layout foundation, combining individual child React components into a cohesive layout under the range of optional layout controls provided as properties.

By default, a Composition will create a 1-column layout,

  1. <Composition>
  2. <Chart timeseriesKey="sensors" duration={5000} />
  3. <Card>Components group goes here</Card>
  4. <SystemDiagram showComplexVariant />
  5. </Composition>

If content being formatted isn't a React component, we use the Box component to wrap the content,

  1. <Composition>
  2. <Box>Good news</Box>
  3. <Box>Bad news</Box>
  4. <Box>No news</Box>
  5. </Composition>

The Composition accepts a wide range of properties which follow industry standard css-grid properties in a type-safe manner. The following example defines a symmetrical 2-column layout with a defined column gap.

When columns are being populated from the set of child components, the grid is populated in a left-to-right top down order.

  1. <Composition templateCols="1fr 1fr" gap={10}>
  2. ...
  3. </Composition>

Better markup with Areas

While building layouts by ordering content inside the Composition is manageable for simple layouts, its strongly recommended to abstract the arrangement of content from the actual markup.

Named areas allow grid definition by name instead of row/column number

We achieve this separation by describing where we want content to be placed, using areas. When multiple identical named area's have been defined, they are automatically grouped.

L-shaped areas are not possible, if such a shape is required (why?) we recommend using a series of nested compositions instead.

  1. const pageLayout = `
  2. Chart Chart
  3. Light Slider
  4. `
  5. export const SensorPage = (props: RouteComponentProps) => {
  6. return (
  7. <Composition areas={pageLayout}>
  8. {Areas => (
  9. <React.Fragment>
  10. <Areas.Chart> ... </Areas.Chart>
  11. <Areas.Light> ... </Areas.Light>
  12. <Areas.Slider> ... </Areas.Slider>
  13. </React.Fragment>
  14. )}
  15. </Composition>
  16. )
  17. }

The pageLayout string describes rows and columns, and the area names are used in the Composition. By describing the arrangement explicitly, different area maps can be provided for different window sizes, and maintaining or reordering the layout is easier.

As the {Areas => ( ... )} function syntax is used, we need to return a single object rather than many.

It is recommend to use React.Fragment as shown but empty tags <> ... </> work too!

Responsive Properties

By combining the responsive behaviour of a breakpoint with the styling power of properties, its possible to define how a specific layout property will behave for a given range of viewports.

Responsive Properties can be used to slim down margins or gap sizing for compact layouts, enlarge content for large-screen use.

  1. <Composition
  2. areas={layoutDescription}
  3. gapMd={30}
  4. gapXs={10}
  5. marginMd={20}
  6. marginXs={5}
  7. >

By default, the rules will apply from the specified breakpoint in a downward priority.

For example, specifying gapLg={20} gapXs={10} means that the gap size is 20px from the lg breakpoint until the window fits under the xs ruleset.

This behaviour can be inverted by providing Layout with the desired defaultBehaviour value.

Responsive Areas

Breakpoints allow you to fundamentally change the layout by switching to a more suitable Area declaration.

  1. const normalLayoutDescription = `
  2. Light Chart
  3. Light Slider
  4. `
  5. const compactLayoutDescription = `
  6. Chart
  7. Slider
  8. Light
  9. `
  1. <Composition
  2. areasXs={smallLayoutDescription}
  3. areasLg={normalLayoutDescription}
  4. gap={10}
  5. >
  6. {Areas => (
  7. <React.Fragment>
  8. <Areas.Chart> ... </Areas.Chart>
  9. <Areas.Light> ... </Areas.Light>
  10. <Areas.Slider> ... </Areas.Slider>
  11. </React.Fragment>
  12. )}
  13. </Composition>

Breakpoint driven visibility

It's also possible to hide components entirely. For example, inline usage instructions/diagrams could be replaced by a streamlined checklist on mobile displays.

Wrapping children components in an Only component will prevent them from rendering when their breakpoint conditions are met.

import { Only, Visible } from 'atomic-layout'
  1. <Only for="lg">This text only draws on large screens</Only>
  2. <Only from={{ minWidth: 500 }} to={{ maxWidth: 900 }}>
  3. This text is visible within a custom range.
  4. </Only>

In some applications, wrapping children in a Visible component might be preferred as it still renders the underlying component, but sets the visibility to 0. This would occur if the layout needs to remain consistent while the object is hidden.

Examples

Provide 3 different examples

  • Sidebar like discord/slack
  • UI which re-arranges completely based on screensize