UX Writing Portfolio

Hi, I'm Elmi, a content & UX writer.

For six years I've made complex things clear: SEO articles read by millions, content guidelines for teams to write by, campaign copy that had to earn a click. UX writing is the same craft at a tighter word count. It's one of my interests, and the work I want to do next.

The samples below are spec pieces, self-directed rewrites of everyday e-commerce flows. Each one shows the problem, the rewrite, and the reasoning behind every word because I believe good microcopy isn't decoration, it's a series of clear decisions about what the user needs to feel and do next. I've written each in English and Bahasa Indonesia to cover global and regional markets.

Native Indonesian · C2 English · 6+ years content & SEO · linkedin.com/in/elmirfa

Sample 01

Payment Failed

Context: The user taps Pay Now and the payment fails. Their single biggest fear in this moment: "Did I just get charged?"

✕ Before

⚠️ Error

Transaction failed. Please try again later.

OK

There is no cause, no next step, and the only button is a dead end. Worst of all it never answers the money in question. Almost always, the user leaves the app to check their bank balance, and the sale is lost.

✓ After

Payment didn't go through

Don't worry, no money was taken. Please verify your card or check your balance.

Try another methodRetry

Why it works

  • Reassure first. "No money was taken" answers the top fear before anything else. No anxiety, no leaving the app.
  • Plain cause, no blame. The next instruction explains without making the user feel stupid or accused.
  • Two real exits. Primary action assumes the method was the problem (most common fix); secondary lets the confident user retry. No dead-end "OK."
  • Title states the fact, not "Error." "Payment didn't go through" is understood in one glance; "Error" lets the user assumes what went wrong.
Bahasa Indonesia

Pembayaran belum berhasil

Tenang, saldo kamu tidak terpotong. Coba verifikasi kartumu atau cek saldomu.

Coba metode lain Ulangi

"Belum berhasil" (not yet successful) instead of a hard "gagal" keeps the door open and feels less fatal. "Kamu" holds the warm, peer tone Indonesian shoppers expect; "Tenang" does the reassurance work "Don't worry" does in English.

Sample 02

Empty Cart

Context: The user opens their cart and it's empty. An empty screen is a missed moment, it can be a wall or a doorway.

✕ Before

Your cart is empty.

A full stop. It describes a state and offers nothing to do next.

✓ After

Your cart's still empty.

Saw something you liked earlier? Let's add here.

Browse deals

Why it works

  • "still empty" reframes emptiness as temporary, not a failure.
  • It adds action words. The action word "add here" recovers intent the user already showed; "Browse deals" gives a fresh start. Both point to a path forward.
  • One primary action. Empty states should make the next tap obvious, not offer a menu.
Bahasa Indonesia

Keranjang kamu masih kosong

Ada barang yang sempat kamu lirik? Yuk tambahkan sekarang.

"Sempat kamu lirik" (that you glanced at) is casual and natural in Indonesian e-commerce, more human than a literal "yang kamu suka." "Promo" outperforms "deals/diskon" as the word Indonesian shoppers actually search and tap.

Sample 03

Notification Priming

Context: A new user finishes signup. The OS notification prompt is one-shot. If they tap "Don't Allow," it's very hard to recover. So we earn the yes before the system asks.

✕ Before — firing the raw system prompt cold

"Shopee" Would Like to Send You Notifications

Don't AllowAllow

No reason given, no purpose known. Most users reflexively decline which causes lost opportunity.

✓ After - a priming screen first

Want first dibs on flash sales?

Turn on notifications and we'll ping you when your favorite items are on sale and when your order's on the move. No spam, promise.

Sounds goodMaybe later

Why it works

  • Asks in value terms, not permission terms. "First dibs on flash sales" is a benefit while "send you notifications" is a chore.
  • Names the two things users actually want: items on sale and order updates.
  • "No spam, promise" pre-empts the real objection.
  • "Maybe later" keeps the door open. Declining here doesn't burn the system prompt, so we can ask again at a better moment (e.g. right after they place an order).
Bahasa Indonesia

Mau tahu duluan kalau ada flash sale?

Nyalakan notifikasi, nanti kami kabari pas barang incaranmu turun harga dan pas pesananmu jalan. Bebas spam, janji.

"Tahu duluan" (know first) captures the FOMO that drives Indonesian flash-sale culture better than a literal translation. "Nanti aja" is the natural casual decline softer and more honest than a formal "Lain kali."

Sample 04

Inline Form Validation: Wrong Phone Number

Context: The user is typing their shipping address at checkout and leaves the phone field in a bad format. A confusing error here stalls the single most important flow in the app.

✕ Before

❌ Invalid input.

Which field? Why invalid? How do I fix it? It tells the user they're wrong without telling them how to be right.

✓ After - inline, under the phone field

Add your number so the courier can reach you (e.g. 0812 3456 7890)

Why it works

  • Guidance is clear. Says whats needed, not what's wrong.
  • Gives the reason ("so the courier can reach you"). It's easier for people to follow rules they understand.
  • Shows the format with a real example instead of describing it. An example parses faster than "must be 10-13 digits."
  • No red "Invalid" shouting unless the user actively submits a broken value.
Bahasa Indonesia

Isi nomor hp kamu biar kurir gampang menghubungi (contoh: 0812 3456 7890)

"Biar kurir gampang menghubungi" uses everyday "biar/gampang" instead of formal "agar/mudah" — it matches how Indonesians actually speak and keeps checkout feeling frictionless rather than bureaucratic.

Sample 05

Variations of Order - Status Notifications

Context: A push series across the order lifecycle. The craft here is consistency and rhythm so the messages should feel like one voice guiding you through every step.

StageBefore (flat)After (voice + value)
Order confirmedYour order has been placed.Order's in! 🎉 We're letting the seller know. We'll ping you when it ships.
ShippedYour order has been shipped.On its way! Your order just left the warehouse. Track it any time from My Orders.
Out for deliveryYour order is out for delivery.Almost there! Your courier's out with your order today. Keep your phone close.
DeliveredYour order has been delivered.Delivered ✅ Your order is in your hand safely. Tap to rate it and help other shoppers.
DelayedYour order is delayed.Running a little late. Your order's held up in transit, we're on it. New estimate: {date}.

Why it works

  • One arc, one voice. Each message picks up where the last left off, so the series feels like a person, not a system.
  • Every message earns the next open by adding a small piece of value like a track link, a heads-up, a new estimate.
  • The delay message leads with honesty and ownership ("we're on it") and ends with the one thing the user wants: a new date. Bad news handled well builds more trust than good news.
  • Emoji used sparingly and only where the moment is genuinely positive (confirmed, delivered). Never on the delay to avoid raising fear or uncertainty.
Bahasa Indonesia
TahapanSebelum (datar)Setelah (voice + value)
Pesanan masukPesananmu sudah diterima.Pesanan masuk! 🎉 Penjual akan menyiapkan pesananmu. Tunggu info pengirimannya ya!
Paket dikirimkanPaketmu sudah dikirimkan.Paket dalam perjalanan! Paket dalam proses pengiriman. Cek status paket di menu Pesanan Saya.
Paket di perjalananPaketmu sedang diantar.Pesanan hampir sampai! Kurir sedang dalam perjalanan. Cek hp kamu secara berkala.
TerkirimPesananmu sudah tiba.Terkirim ✅ Paket sudah diterima dengan aman. Klik di sini untuk memberikan penilaian.
TerlambatPesananmu mengalami keterlambatan.Pengiriman terkendala. Paketmu mengalami kendala pengiriman, kami pantau secara berkala. Estimasi tiba: {tanggal}.

Sample 06

Search: no results

Context: The user searches for something with no match. A blank "0 results" is the fastest way to lose a shopper who came with intent.

✕ Before

No results found for "{query}".

A dead stop that blames the search. 50:50 chance the user bounces and leaves the app.

✓ After

No result for "{query}", yet

Check the spelling, try a more general word, or browse what's close below.

See related items

Why it works

  • "yet" softens the miss and hints the catalog grows.
  • Three concrete recovery moves (spelling, broaden, browse) cover the real causes of a zero-result search.
  • It still offers product, not just advice. Phrase "related items" keeps the shopping going instead of ending the session.
Bahasa Indonesia

Belum ada yang pas untuk "{query}"

Coba cek ejaan, pakai kata yang lebih umum, atau lihat barang serupa di bawah.

"Yang pas" (something that fits) is warmer and more natural than a literal "hasil" (results). Indonesian shoppers respond to "barang serupa" as more concrete and familiar over an abstract "rekomendasi."

Sample 07

Voucher Won't Apply: Constraint Feedback

Context: The user pastes a voucher code and it's rejected. A hot moment. They expected to save money but failed. Vague copy here feels like a bait-and-switch.

✕ Before

Voucher invalid.

"Invalid" could mean expired, wrong code, minimum not met, or wrong category. The user can't tell if they made a typo or if Shopee tricked them.

✓ After - inline under the voucher field

This voucher needs a minimum order of Rp150,000. Shop Rp32.000 more to unlock Rp50,000 off.

Keep shopping

Why it works

  • Names the exact reason (minimum not met) instead of a catch-all "invalid."
  • Does the math for them ("Shop Rp32,000 more") and reframes the gap as an unlock. A clear reward after a task.
  • Turns a stall into momentum with a forward action.
  • For an expired or wrong code, the same pattern applies: state the real cause "This code expired on {date}", so the user knows clearly what the issue is.


Bahasa Indonesia

Voucher ini berlaku untuk minimal belanja Rp150.000. Belanja Rp32.000 lagi buat dapat potongan Rp50.000.

State the amount gap clearly and tell the next step directly to avoid ambiguity. Then "buat dapat" keeps the reward casual and motivating.

Sample 08

Cancel Order - Destructive Action Confirmation

Context: The user taps "Cancel Order." This is destructive and often irreversible. UX writing's job here is to make sure they mean it without nagging the people who do.

✕ Before

Are you sure you want to cancel?

NoYes

"Are you sure?" carries no information. "Yes/No" forces the user to map the question back to the action which is basically a classic source of mis-taps. And it states no consequence.

✓ After

Cancel this order?

Your refund of Rp200,000 goes back to ShopeePay within 24 hours. This can't be undone.

Keep my orderCancel order

Why it works

  • Buttons name the action, not Yes/No. The user can act on the buttons alone without re-reading the question. Also the safe choice ("Keep my order") is phrased to feel like the easy default.
  • States the real consequence: refund amount, where it goes, how long. Exactly what the user needs to know.
  • "This can't be undone" sets the right weight. An honest friction on an irreversible action.
Bahasa Indonesia

Mau batalkan pesanan ini?

Dana Rp200.000 akan dikembalikan ke ShopeePay dalam 24 jam. Tindakan ini tidak bisa dibatalkan.

"Dana … dikembalikan" is the standard, trusted phrasing for refunds in Indonesian e-commerce. Money and irreversible actions are where users want clarity and seriousness, not jokes.

Sample 09

OTP Verification: entry + error

Context: The user is logging in and must enter a one-time code sent by SMS. OTP screens are pure friction, the writing's only job is to remove confusion fast.

✕ Before

Enter OTP.

Wrong OTP. (on error)

"OTP" is jargon many users don't know. "Wrong OTP" doesn't say what to do: re-type? Resend? Wait?

✓ After

Enter the 6-digit code

We sent it by SMS to •••• •••• 7890. It can take a few seconds to arrive.

Didn't get it? Resend in 0:30

(on wrong entry) That code doesn't match. Double-check the SMS, or resend a new one.

Why it works

  • "6-digit code" instead of "OTP": plain language anyone understands.
  • Shows where it went (masked number) so the user knows which phone to check, and sets the expectation that it's not instant.
  • The resend timer in button to help the user navigates the help in an obvious way so the user won't get stuck.
  • The error says what to do, not just that they're wrong.
Bahasa Indonesia

Masukkan 6 digit kode

Kode sudah dikirimkan lewat SMS ke •••• •••• 7890. Butuh beberapa detik untuk sampai.

Belum dapat? Kirim ulang dalam 0:30

"Kode 6 digit" drops the English "OTP" jargon for clarity. "Belum dapat?" (haven't got it yet?) is friendlier and more natural than a literal "Tidak menerima?"

The system behind the samples

Tone of Voice Mini-Guide

A one-page voice guide with a set of rules for the team to write by.

Our voice in three words

Clear   Warm   On your side

We are…We're not…In practice
ClearClever for its own sakeSay "Payment didn't go through," not "Oops, something went wrong!"
WarmOverfamiliar"kamu," contractions, the occasional emoji but never slang that excludes
On your sideCorporateReassure first ("no money was taken"), explain the why, and always give a next step

Three house rules

  • Lead with the user's biggest question. In a payment error, that's "is my money safe?" Then answer it in the first line.
  • Never a dead end. Every message has a way forward. No dead-end "OK."
  • Say what to do, not what went wrong. "Add your number so the courier can reach you" beats "Invalid input."

Words we use / words we avoid

UseAvoidWhy
didn't go throughfailed / errorsofter, less final
no money was taken(silence)answers the top fear
Try another methodOKa real next step
Running a little lateDELAYEDhonest without alarming

Spec/concept work by Elmi Rahmatika. It's created for portfolio purposes, not shipped Shopee copy. Written in English and Bahasa Indonesia.