diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 227e585..cee1e0b 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -2,6 +2,7 @@ pub(crate) use chars::*;
 pub(crate) use comment_whitespace::*;
 pub(crate) use interpolation::*;
 pub(crate) use number::*;
+pub(crate) use peek_until::*;
 pub(crate) use read_until::*;
 pub(crate) use strings::*;
 pub(crate) use variables::*;
@@ -10,6 +11,7 @@ mod chars;
 mod comment_whitespace;
 mod interpolation;
 mod number;
+mod peek_until;
 mod read_until;
 mod strings;
 mod variables;
diff --git a/src/utils/peek_until.rs b/src/utils/peek_until.rs
new file mode 100644
index 0000000..0eef894
--- /dev/null
+++ b/src/utils/peek_until.rs
@@ -0,0 +1,108 @@
+use std::iter::Iterator;
+
+use peekmore::PeekMoreIterator;
+
+use crate::Token;
+
+use super::IsWhitespace;
+
+pub(crate) fn peek_until_closing_curly_brace<I: Iterator<Item = Token>>(
+    toks: &mut PeekMoreIterator<I>,
+) -> Vec<Token> {
+    let mut t = Vec::new();
+    let mut nesting = 0;
+    while let Some(tok) = toks.peek() {
+        match tok.kind {
+            q @ '"' | q @ '\'' => {
+                t.push(*toks.peek().unwrap());
+                toks.move_forward(1);
+                t.extend(peek_until_closing_quote(toks, q));
+            }
+            '{' => {
+                nesting += 1;
+                t.push(*toks.peek().unwrap());
+                toks.move_forward(1);
+            }
+            '}' => {
+                if nesting == 0 {
+                    break;
+                } else {
+                    nesting -= 1;
+                    t.push(*toks.peek().unwrap());
+                    toks.move_forward(1);
+                }
+            }
+            '/' => {
+                let next = *toks.peek_forward(1).unwrap();
+                match toks.peek().unwrap().kind {
+                    '/' => peek_until_newline(toks),
+                    _ => t.push(next),
+                };
+                continue;
+            }
+            _ => {
+                t.push(*toks.peek().unwrap());
+                toks.move_forward(1);
+            }
+        }
+    }
+    peek_whitespace(toks);
+    t
+}
+
+fn peek_until_closing_quote<I: Iterator<Item = Token>>(
+    toks: &mut PeekMoreIterator<I>,
+    q: char,
+) -> Vec<Token> {
+    let mut t = Vec::new();
+    while let Some(tok) = toks.peek() {
+        match tok.kind {
+            '"' if q == '"' => {
+                t.push(*tok);
+                toks.move_forward(1);
+                break;
+            }
+            '\'' if q == '\'' => {
+                t.push(*tok);
+                toks.move_forward(1);
+                break;
+            }
+            '\\' => {
+                t.push(*tok);
+                t.push(*toks.peek_forward(1).unwrap());
+            }
+            '#' => {
+                t.push(*tok);
+                let next = toks.peek().unwrap();
+                if next.kind == '{' {
+                    t.push(*toks.peek_forward(1).unwrap());
+                    t.append(&mut peek_until_closing_curly_brace(toks));
+                }
+            }
+            _ => t.push(*tok),
+        }
+        toks.move_forward(1);
+    }
+    t
+}
+
+fn peek_until_newline<I: Iterator<Item = Token>>(toks: &mut PeekMoreIterator<I>) {
+    while let Some(tok) = toks.peek() {
+        if tok.kind == '\n' {
+            break;
+        }
+        toks.move_forward(1);
+    }
+}
+
+fn peek_whitespace<I: Iterator<Item = W>, W: IsWhitespace>(s: &mut PeekMoreIterator<I>) -> bool {
+    let mut found_whitespace = false;
+    while let Some(w) = s.peek() {
+        if !w.is_whitespace() {
+            break;
+        }
+        found_whitespace = true;
+        s.move_forward(1);
+    }
+    found_whitespace
+}
diff --git a/src/utils/read_until.rs b/src/utils/read_until.rs
index 0bcbeed..94e5c1b 100644
--- a/src/utils/read_until.rs
+++ b/src/utils/read_until.rs
@@ -68,6 +68,10 @@ pub(crate) fn read_until_closing_curly_brace<I: Iterator<Item = Token>>(
                 };
                 continue;
             }
+            '(' => {
+                t.push(toks.next().unwrap());
+                t.extend(read_until_closing_paren(toks));
+            }
             _ => t.push(toks.next().unwrap()),
         }
     }
diff --git a/src/value/css_function.rs b/src/value/css_function.rs
index 5832997..877de43 100644
--- a/src/value/css_function.rs
+++ b/src/value/css_function.rs
@@ -3,7 +3,7 @@ use peekmore::PeekMoreIterator;
 use crate::error::SassResult;
 use crate::scope::Scope;
 use crate::selector::Selector;
-use crate::utils::{devour_whitespace, parse_interpolation};
+use crate::utils::{devour_whitespace, parse_interpolation, peek_until_closing_curly_brace};
 use crate::Token;
 
 pub(crate) fn eat_calc_args<I: Iterator<Item = Token>>(
@@ -78,3 +78,125 @@ pub(crate) fn eat_progid<I: Iterator<Item = Token>>(
     }
     Ok(string)
 }
+
+pub(crate) fn try_eat_url<I: Iterator<Item = Token>>(
+    toks: &mut PeekMoreIterator<I>,
+    scope: &Scope,
+    super_selector: &Selector,
+) -> SassResult<Option<String>> {
+    let mut buf = String::from("url(");
+    let mut peek_counter = 0;
+    while let Some(tok) = toks.peek() {
+        let kind = tok.kind;
+        toks.move_forward(1);
+        peek_counter += 1;
+        if kind == '!'
+            || kind == '%'
+            || kind == '&'
+            || (kind >= '*' && kind <= '~')
+            || kind as u32 >= 0x0080
+        {
+            buf.push(kind);
+        } else if kind == '\\' {
+            buf.push_str(&peek_escape(toks)?);
+        } else if kind == '#' {
+            let next = toks.peek();
+            if next.is_some() && next.unwrap().kind == '{' {
+                toks.move_forward(1);
+                peek_counter += 1;
+                let (interpolation, count) = peek_interpolation(toks, scope, super_selector)?;
+                peek_counter += count;
+                buf.push_str(&match interpolation.node {
+                    Value::Ident(s, ..) => s,
+                    v => v.to_css_string(interpolation.span)?,
+                });
+            } else {
+                buf.push('#');
+            }
+        } else if kind == ')' {
+            buf.push(')');
+            for _ in 0..=peek_counter {
+                toks.next();
+            }
+            return Ok(Some(buf));
+        } else {
+            break;
+        }
+    }
+    toks.reset_view();
+    Ok(None)
+}
+
+use crate::utils::{as_hex, hex_char_for, is_name};
+
+fn peek_escape<I: Iterator<Item = Token>>(toks: &mut PeekMoreIterator<I>) -> SassResult<String> {
+    let mut value = 0;
+    let first = match toks.peek() {
+        Some(t) => t,
+        None => return Ok(String::new()),
+    };
+    if first.kind == '\n' {
+        return Err(("Expected escape sequence.", first.pos()).into());
+    } else if first.kind.is_ascii_hexdigit() {
+        for _ in 0..6 {
+            let next = match toks.peek_forward(1) {
+                Some(t) => t,
+                None => break,
+            };
+            if !next.kind.is_ascii_hexdigit() {
+                break;
+            }
+            value *= 16;
+            value += as_hex(toks.next().unwrap().kind as u32)
+        }
+        if toks.peek().is_some() && toks.peek().unwrap().kind.is_whitespace() {
+            toks.peek_forward(1);
+        }
+    } else {
+        value = toks.peek_forward(1).unwrap().kind as u32;
+    }
+
+    // tabs are emitted literally
+    // TODO: figure out where this check is done
+    // in the source dart
+    if value == 0x9 {
+        return Ok("\\\t".to_string());
+    }
+
+    let c = std::char::from_u32(value).unwrap();
+    if is_name(c) {
+        Ok(c.to_string())
+    } else if value <= 0x1F || value == 0x7F {
+        let mut buf = String::with_capacity(4);
+        buf.push('\\');
+        if value > 0xF {
+            buf.push(hex_char_for(value >> 4));
+        }
+        buf.push(hex_char_for(value & 0xF));
+        buf.push(' ');
+        Ok(buf)
+    } else {
+        Ok(format!("\\{}", c))
+    }
+}
+
+use crate::value::Value;
+use codemap::Spanned;
+
+fn peek_interpolation<I: Iterator<Item = Token>>(
+    toks: &mut PeekMoreIterator<I>,
+    scope: &Scope,
+    super_selector: &Selector,
+) -> SassResult<(Spanned<Value>, usize)> {
+    let vec = peek_until_closing_curly_brace(toks);
+    let peek_counter = vec.len();
+    toks.move_forward(1);
+    let val = Value::from_vec(vec, scope, super_selector)?;
+    Ok((
+        Spanned {
+            node: val.node.eval(val.span)?.node.unquote(),
+            span: val.span,
+        },
+        peek_counter,
+    ))
+}
diff --git a/src/value/parse.rs b/src/value/parse.rs
index 6e94f3d..a85d7d9 100644
--- a/src/value/parse.rs
+++ b/src/value/parse.rs
@@ -10,7 +10,7 @@ use codemap::{Span, Spanned};
 
 use peekmore::{PeekMore, PeekMoreIterator};
 
-use super::css_function::{eat_calc_args, eat_progid};
+use super::css_function::{eat_calc_args, eat_progid, try_eat_url};
 
 use crate::args::eat_call_args;
 use crate::builtin::GLOBAL_FUNCTIONS;
@@ -530,7 +530,13 @@ impl Value {
                                 }
                                 // "min" => {}
                                 // "max" => {}
-                                // "url" => {}
+                                "url" => match try_eat_url(toks, scope, super_selector)? {
+                                    Some(val) => s = val,
+                                    None => s.push_str(
+                                        &eat_call_args(toks, scope, super_selector)?
+                                            .to_css_string(scope, super_selector)?,
+                                    ),
+                                },
                                 _ => s.push_str(
                                     &eat_call_args(toks, scope, super_selector)?
                                         .to_css_string(scope, super_selector)?,
diff --git a/tests/url.rs b/tests/url.rs
new file mode 100644
index 0000000..c28dcfb
--- /dev/null
+++ b/tests/url.rs
@@ -0,0 +1,91 @@
+#![cfg(test)]
+
+#[macro_use]
+mod macros;
+
+test!(
+    arithmetic_both_space,
+    "a {\n  color: url(1 + 2);\n}\n",
+    "a {\n  color: url(3);\n}\n"
+);
+test!(
+    arithmetic_space_right,
+    "a {\n  color: url(1+ 2);\n}\n",
+    "a {\n  color: url(3);\n}\n"
+);
+test!(
+    arithmetic_space_left,
+    "a {\n  color: url(1 +2);\n}\n",
+    "a {\n  color: url(3);\n}\n"
+);
+test!(
+    arithmetic_no_space,
+    "a {\n  color: url(1+2);\n}\n",
+    "a {\n  color: url(1+2);\n}\n"
+);
+test!(
+    silent_comment,
+    "a {\n  color: url(//some/absolute/path);\n}\n"
+);
+test!(
+    multiline_comment,
+    "a {\n  color: url(/*looks-like-a*/comment);\n}\n"
+);
+test!(plain_css_function, "a {\n  color: url(fn(\"s\"));\n}\n");
+test!(
+    builtin_function,
+    "a {\n  color: url(if(true, \"red.png\", \"blue.png\"));\n}\n",
+    "a {\n  color: url(\"red.png\");\n}\n"
+);
+test!(
+    user_defined_function,
+    "$file-1x: \"budge.png\";\n@function fudge($str) {\n  @return \"assets/fudge/\"+$str;\n}\n\na {\n  color: url(fudge(\"#{$file-1x}\"));\n}\n",
+    "a {\n  color: url(\"assets/fudge/budge.png\");\n}\n"
+);
+test!(
+    unquoted_interpolation,
+    "a {\n  color: url(hello-#{world}.png);\n}\n",
+    "a {\n  color: url(hello-world.png);\n}\n"
+);
+test!(
+    quoted_interpolation,
+    "a {\n  color: url(\"hello-#{world}.png\");\n}\n",
+    "a {\n  color: url(\"hello-world.png\");\n}\n"
+);
+test!(simple_forward_slash, "a {\n  color: url(foo/bar.css);\n}\n");
+test!(http_url, "a {\n  color: url(http://foo.bar.com);\n}\n");
+test!(
+    google_fonts_url,
+    "a {\n  color: url(http://fonts.googleapis.com/css?family=Karla:400,700,400italic|Anonymous+Pro:400,700,400italic);\n}\n"
+);
+test!(
+    interpolation_in_http_url,
+    "a {\n  color: url(http://blah.com/bar-#{foo}.css);\n}\n",
+    "a {\n  color: url(http://blah.com/bar-foo.css);\n}\n"
+);
+test!(
+    many_forward_slashes,
+    "a {\n  color: url(http://box_////fudge.css);\n}\n"
+);
+test!(
+    url_whitespace,
+    "a {\n  color: url(       1      );\n}\n",
+    "a {\n  color: url(1);\n}\n"
+);
+test!(
+    url_newline,
+    "a {\n  color: url(\n);\n}\n",
+    "a {\n  color: url();\n}\n"
+);
+test!(url_comma_list, "a {\n  color: url(1, 2, a, b, c);\n}\n");
+test!(
+    url_contains_only_interpolation,
+    "a {\n  color: url(#{1 + 2});\n}\n",
+    "a {\n  color: url(3);\n}\n"
+);
+test!(
+    url_begins_with_interpolation,
+    "a {\n  color: url(#{http}://foo);\n}\n",
+    "a {\n  color: url(http://foo);\n}\n"
+);
+test!(url_dot_dot, "a {\n  color: url(../foo/bar/..baz/);\n}\n");