Size, Spacing and Borders

Density-independent pixels, padding and margin, percentage sizes, aspectRatio, borderRadius, and how shadows actually work.

Syntax{ paddingHorizontal: 16, width: '100%', aspectRatio: 16 / 9, borderRadius: 12 }

Every unitless number in a style is a density-independent pixel, written dp. The system multiplies it by the screen's pixel density, so 16dp is physically the same size on a cheap phone and on a flagship. PixelRatio.get() returns that density: 2 on an iPhone SE or 11, 3 on an iPhone X or a Pixel 2, 1.5 on an hdpi Android device.

Spacing

Padding and margin exist, with the usual per-side keys plus paddingHorizontal, paddingVertical and the writing-direction pair paddingStart and paddingEnd. paddingHorizontal: 16 is shorthand for paddingLeft: 16, paddingRight: 16 and nothing more. Two differences from CSS matter: there is no margin collapsing, so adjacent margins add up, and boxSizing defaults to border-box rather than content-box, so padding and border are counted inside width instead of being added to it. React Native does expose a boxSizing style prop, so boxSizing: 'content-box' switches a view to the CSS default when you need it.

Size

width and height take a number of dp or a percentage string measured against the parent. The docs are explicit that only points and percentages work; em, rem, vh and vw do not exist, and neither does calc(). aspectRatio fills in whichever dimension you left undefined, so width: '100%' with aspectRatio: 16 / 9 gives a responsive video box with no height arithmetic.

Borders and shadows

borderRadius has per-corner longhands including logical ones such as borderStartStartRadius. Shadows are the ugly part: iOS reads shadowColor, shadowOffset, shadowOpacity and shadowRadius, while Android reads elevation. There is also a boxShadow prop, but it needs the New Architecture, and the docs limit outset shadows to Android 9 and above.

Example

Example · tsx
import { Platform, StyleSheet, Text, View } from 'react-native';

export default function BoxModelDemo() {
  return (
    <View style={styles.screen}>
      <View style={styles.card}>
        {/* Height comes from aspectRatio, so no number has to be guessed */}
        <View style={styles.cover} />
        <Text style={styles.caption}>16:9 cover, 12dp radius</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  screen: { flex: 1, padding: 16, backgroundColor: '#f2f2f7' },
  card: {
    // The box is border-box: this 12dp padding is inside the card's width.
    padding: 12,
    borderRadius: 16,
    backgroundColor: '#ffffff',
    // One shadow, two platform spellings. Neither works on the other OS.
    ...Platform.select({
      ios: {
        shadowColor: '#000000',
        shadowOffset: { width: 0, height: 2 }, // an object, not a number
        shadowOpacity: 0.15,
        shadowRadius: 6,
      },
      android: { elevation: 3 },
      default: {},
    }),
  },
  cover: {
    width: '100%',        // percentages are measured against the parent
    aspectRatio: 16 / 9,  // fills in the undefined dimension
    borderRadius: 12,
    backgroundColor: '#d1d1d6',
  },
  caption: { marginTop: 8, fontSize: 13, color: '#636366' },
});

When to use it

  • Give a media thumbnail a 16:9 shape without computing a pixel height.
  • Add a card shadow that appears on both iOS and Android.
  • Round the corners of a group and clip its children to that radius.
  • Replace per-child margins with padding plus gap so spacing stays consistent.

More examples

Longhand padding, logical sides, and a hairline divider

Two settings rows of exactly the same height, separated by a one-pixel line, inside a rounded white group. The two row styles are written both ways to show that the shorthand expands to four keys and nothing else. overflow: 'hidden' on the group is what makes the children respect its corner radius.

Example · tsx
import { StyleSheet, Text, View } from 'react-native';

export default function SettingsRows() {
  return (
    <View style={styles.group}>
      <View style={styles.rowShorthand}>
        <Text>Notifications</Text>
      </View>
      <View style={styles.divider} />
      <View style={styles.rowLonghand}>
        <Text>Sounds</Text>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  group: { borderRadius: 12, backgroundColor: '#ffffff', overflow: 'hidden' },
  // These two rows are identical; the second spells out the first.
  rowShorthand: { paddingHorizontal: 16, paddingVertical: 12 },
  rowLonghand: {
    paddingLeft: 16,
    paddingRight: 16,
    paddingTop: 12,
    paddingBottom: 12,
  },
  // paddingStart/paddingEnd flip with the writing direction; left/right do not.
  divider: { height: StyleSheet.hairlineWidth, backgroundColor: '#c6c6c8' },
});

A square avatar grid without a single fixed height

Three squares filling the row with 8dp between them, staying square on a narrow phone and on a tablet. A hardcoded height would be wrong on both. Combining flex: 1 with aspectRatio is the standard way to size tiles, map previews and media thumbnails.

Example · tsx
import { StyleSheet, View } from 'react-native';

export default function AvatarGrid() {
  return (
    <View style={styles.row}>
      <View style={styles.cell} />
      <View style={styles.cell} />
      <View style={styles.cell} />
    </View>
  );
}

const styles = StyleSheet.create({
  row: { flexDirection: 'row', gap: 8, padding: 8 },
  cell: {
    flex: 1,            // each cell takes a third of the row
    aspectRatio: 1,     // height follows the measured width: a square
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#c7c7cc',
    backgroundColor: '#e5e5ea',
  },
});

Discussion

  • Be the first to comment on this lesson.