Styling NSTextField: A Guide for Designers

John Marstall
Nov 23, 2015 · 5 min read

On the web, styling text fields is straightforward. To produce a text field like this:

A text field styled with CSS

… you’d write the following CSS:

input {
    font-size: 13px;
    border-radius: 5px;
    color: #363c59;
    border: 1px solid #657091;
    background-color: #c5cbe0;
    }

When developing for Mac, this kind of customization isn’t so simple. The NSTextField class exposes only a handful of cosmetic controls, and some of them can produce unexpected effects. Here’s what I learned from watching developers tackle the issue in a recent project.

Assume inputTextField is an NSTextField connected to a ViewController.swift via an IBOutlet. inputTextField is bordered and has a background color, by default, in Interface Builder.

Note that NSTextView is a different beast and some of these tips will not apply to it.

Let’s say we’ve defined three colors for our field’s border, background, and text:

let fieldBackgroundColor = NSColor(
                                calibratedHue: 230/360,
                                saturation: 0.15,
                                brightness: 0.85,
                                alpha: 1)
let fieldBorderColor = NSColor(
                                calibratedHue: 230/360,
                                saturation: 0.35,
                                brightness: 0.50,
                                alpha: 1)
let fieldTextColor = NSColor(
                                calibratedHue: 230/360,
                                saturation: 0.40,
                                brightness: 0.35,
                                alpha: 1)

We quickly realize there’s no class-specific way to address border color. Background and text colors seem easy enough, though:

override func viewDidLoad() {
    super.viewDidLoad()
    inputTextField.backgroundColor = fieldBackgroundColor
    inputTextField.textColor = fieldTextColor
}

But that results in something that’s not visually quite right. There’s a visible gap just within the border, and of course we haven’t touched border color or corner radius.

First attempt

A natural next step would be to tackle the text field’s layer. (A layer is a drawing area that sits on top of the object’s basic appearance. You can learn more about customizing layers in Working with IBDesignable.)

override func viewDidLoad(){
    super.viewDidLoad()
    inputTextField.wantsLayer = true
    let textFieldLayer = CALayer()
    inputTextField.layer = textFieldLayer
    inputTextField.layer?.backgroundColor = fieldBackgroundColor.CGColor
    inputTextField.layer?.borderColor = fieldBorderColor.CGColor
    inputTextField.layer?.borderWidth = 1
    inputTextField.layer?.cornerRadius = 5
    inputTextField.textColor = fieldTextColor
    }
Second attempt

That gets us the desired border and rounding but leaves an ugly white rectangle. We’re picking up the white background color set on the text field in the .xib. We might try removing that by unchecking “Draws Background” or by setting:

inputTextField.drawsBackground = false
Third attempt

For this example, that’s enough to get the job done. In some cases, we found disabling the built-in background color left an ugly hole. This can be a sign of an issue in layer backing. However, if you’ve tried everything and can’t fix it, there’s another solution: double-bumping the background color.

“Double bump” is a print industry term that refers to printing an area with the same color twice. Likewise, we’re going to overlay a background color on a background color, like so:

override fun viewDidLoad() {
    super.viewDidLoad()
    inputTextField.wantsLayer = true
    let textFieldLayer = CALayer()
    inputTextField.layer = textFieldLayer
    inputTextField.backgroundColor = fieldBackgroundColor
    inputTextField.layer?.backgroundColor = fieldBackgroundColor.CGColor
    inputTextField.layer?.borderColor = fieldBorderColor.CGColor
    inputTextField.layer?.borderWidth = 1
    inputTextField.layer?.cornerRadius = 5
    inputTextField.textColor = fieldTextColor
}

That should cover just about every case.

We might also like to customize the color of any placeholder text in the field. That’s trickier, but it can be done.

We’ll need to add a color constant for it, of course:

let placeholderTextColor = NSColor(
                            calibratedHue: 230/360,
                            saturation: 0.40,
                            brightness: 0.35,
                            alpha: 0.5)

To tie it to the inputTextField, we need to include it in a set of text attributes. We create that set of text attributes as an NSDictionary.

let placeholderAttributes: [ : ] = [ : ]

NSDictionaries are somewhat like analogies. “This thing is to this thing as that thing is to that thing.” We’re saying here the relationships in placeholderAttributes will all follow a certain pattern, and that pattern is:

let placeholderAttributes: [ String: AnyObject ] = [ : ]

Every attribute in our placeholderAttributes dictionary will consist of a String naming the attribute, and some kind of object (AnyObject, we’re leaving the door wide open for ourselves in case we want to add other arbitrary attributes which require things besides colors) which holds the attribute’s value.

The attribute we care about here is whatever system attribute name will best match up with the placeholderTextColor we defined above. That turns out to be NSForegroundColorAttributeName.

let placeholderAttributes: [String: AnyObject] = [
    NSForegroundColorAttributeName: placeholderTextColor
]

Before we can apply these attributes to our inputTextField, we have to roll them into an NSAttributedString. The attributed string will use our placeholderAttributes for its attributes and the text “Add text here” for its default string.

let placeholderAttributedString = NSAttributedString(string: “Add text here”, attributes: placeholderAttributes)

Now, finally, we can connect the dots back to inputTextField.

inputTextField.placeholderAttributedString =  placeholderAttributedString

String attributes offer many options besides text color. You can do all sorts of styling with them: typographic effects like ligatures and baseline adjustment, shadow and stroke effects, and more mundane things like underlines and strikethroughs.

If you find that using the attributed string has bumped your placeholder text off its intended baseline, you can counter that with a paragraph style. Create an NSMutableParagraphStyle and set minimumLineHeight and maximumLineHeight to the same value:

let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.minimumLineHeight = 17.0
paragraphStyle.maximumLineHeight = 17.0

Add the style to the whole length of your placeholderAttributedString using the addAttribute function:

placeholderAttributedString.addAttribute(NSParagraphStyleAttributeName, value: paragraphStyle, range: NSRange(location: 0,length: placeholderAttributedString.length))

The specific minimum and maximum values will depend on the field’s text size. Start with values around 10.0 and increase or decrease from there to see what effect you’re having.

Make sure you only add placeholderAttributedString to your text field at the end, after applying all styles:

inputTextField.placeholderAttributedString =  placeholderAttributedString

These tips will probably cover most of the typical styling you’ll want to do with NSTextField. A word of caution: you may be tempted at some point to disable NSTextField’s default bordered appearance (either in Interface Builder or in code) in order to simplify supplying your own. If you do this, the field’s text metrics will change dramatically. It’s much simpler to keep the field bordered and draw “over” the defaults if you can.

(You can download the example project here.)

BPXL Craft

Design and technology articles from the Black Pixel team. blackpixel.com

John Marstall

Written by

Staff Designer for Black Pixel.

BPXL Craft

Design and technology articles from the Black Pixel team. blackpixel.com